From e4bc14a1598375c3280558f185d5382f2d05bcbf Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Wed, 7 Feb 2018 13:41:40 -0800 Subject: [PATCH 01/32] experiements with workspace --- .../OpenApiStreamReader.cs | 115 +++++++++++++++--- .../OpenApiReferenceResolver.cs} | 7 +- .../OpenApiRemoteReferenceCollector.cs | 19 +++ .../Models/OpenApiDocument.cs | 7 +- .../Services/OpenApiWorkspace.cs | 28 +++++ 5 files changed, 152 insertions(+), 24 deletions(-) rename src/Microsoft.OpenApi.Readers/{Resolver.cs => Services/OpenApiReferenceResolver.cs} (97%) create mode 100644 src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index e5863d3cb..7389bca5b 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.Interface; +using Microsoft.OpenApi.Readers.Services; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Validations; using SharpYaml; @@ -36,10 +39,69 @@ public OpenApiStreamReader(OpenApiReaderSettings settings = null) /// Returns diagnostic object containing errors detected during parsing /// Instance of newly created OpenApiDocument public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) + { + diagnostic = new OpenApiDiagnostic(); + + // Parse the OpenAPI Document + OpenApiDocument document = ParseStream(input, diagnostic); + + // Resolve References if requested + switch (_settings.ReferenceResolution) + { + case ReferenceResolutionSetting.ResolveRemoteReferences: + throw new ArgumentException("Cannot resolve remote references using synchronous Read method, use ReadAsync instead"); + case ReferenceResolutionSetting.ResolveLocalReferences: + ResolveLocalReferences(document); + break; + case + ReferenceResolutionSetting.DoNotResolveReferences: + break; + } + + ValidateDocument(diagnostic, document); + + return document; + } + + /// + /// Reads the stream input and parses it into an Open API document. + /// Remote references are also loaded as OpenApiDocuments and OpenApiElements and stored in the OpenApiWorkspace + /// + /// Stream containing OpenAPI description to parse. + /// Returns diagnostic object containing errors detected during parsing. Caller must provide an instance. + /// Container for the set of OpenAPIDocuments and OpenApiElements referenced. Provide a previously used workspace to avoid re-parsing common documents. + /// + /// Will be made public once finalized. + internal async Task ReadAsync(Stream input, OpenApiDiagnostic diagnostic, OpenApiWorkspace openApiWorkspace = null) + { + + // Parse the OpenAPI Document + OpenApiDocument document = ParseStream(input, diagnostic); + + // Resolve References if requested + switch (_settings.ReferenceResolution) + { + case ReferenceResolutionSetting.ResolveRemoteReferences: + await ResolveAllReferences(document); + break; + case ReferenceResolutionSetting.ResolveLocalReferences: + ResolveLocalReferences(document); + break; + case + ReferenceResolutionSetting.DoNotResolveReferences: + break; + } + + ValidateDocument(diagnostic, document); + + return document; + } + + + private OpenApiDocument ParseStream(Stream input, OpenApiDiagnostic diagnostic) { ParsingContext context; YamlDocument yamlDocument; - diagnostic = new OpenApiDiagnostic(); // Parse the YAML/JSON try @@ -58,32 +120,20 @@ public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) }; // Parse the OpenAPI Document - var document = context.Parse(yamlDocument, diagnostic); - - // Resolve References if requested - switch (_settings.ReferenceResolution) - { - case ReferenceResolutionSetting.ResolveRemoteReferences: - case ReferenceResolutionSetting.ResolveLocalReferences: - var resolver = new OpenApiReferenceResolver(document); - var walker = new OpenApiWalker(resolver); - walker.Walk(document); - break; - case - ReferenceResolutionSetting.DoNotResolveReferences: - break; - } + return context.Parse(yamlDocument, diagnostic); + } + private void ValidateDocument(OpenApiDiagnostic diagnostic, OpenApiDocument document) + { // Validate the document var errors = document.Validate(_settings.RuleSet); foreach (var item in errors) { diagnostic.Errors.Add(new OpenApiError(item.ErrorPath, item.ErrorMessage)); - } - - return document; + } } + /// /// Helper method to turn streams into YamlDocument /// @@ -100,5 +150,32 @@ internal static YamlDocument LoadYamlDocument(Stream input) } return yamlDocument; } + + private static Task ResolveAllReferences(OpenApiDocument document) + { + // Collect remote references by walking document + var referenceCollector = new OpenApiRemoteReferenceCollector(); + var collectorWalker = new OpenApiWalker(referenceCollector); + + // Load References into a workspace + var openApiWorkspace = new OpenApiWorkspace(); + openApiWorkspace.LoadAsync(referenceCollector.References).Wait(); + + // Attach Workspace to primary document + document.Workspace = openApiWorkspace; + + // Load all remote references into OpenApiWorkspace + var resolver = new OpenApiReferenceResolver(document, true); + var walker = new OpenApiWalker(resolver); + walker.Walk(document); + } + + private static void ResolveLocalReferences(OpenApiDocument document) + { + var resolver = new OpenApiReferenceResolver(document, false); + var walker = new OpenApiWalker(resolver); + walker.Walk(document); + } + } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Readers/Resolver.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs similarity index 97% rename from src/Microsoft.OpenApi.Readers/Resolver.cs rename to src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs index fee861cfc..70b8d8cbf 100644 --- a/src/Microsoft.OpenApi.Readers/Resolver.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs @@ -8,7 +8,7 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; -namespace Microsoft.OpenApi.Readers +namespace Microsoft.OpenApi.Readers.Services { /// /// This class is used to walk an OpenApiDocument and convert unresolved references to references to populated objects @@ -36,9 +36,9 @@ public override void Visit(OpenApiComponents components) { ResolveMap(components.Schemas); } - // TODO: Resolve Paths - // TODO: Resolve Callbacks + // TODO: Resolve Paths (Only external) + // TODO: Resolve Callbacks (need to add to visitor) /// /// Resolve all references used in an operation @@ -100,7 +100,6 @@ public override void Visit(OpenApiSchema schema) ResolveObject(schema.AdditionalProperties, r => schema.AdditionalProperties = r); } - /// /// Replace references to tags with either tag objects declared in components, or inline tag object /// diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs new file mode 100644 index 000000000..c62e48373 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; + +namespace Microsoft.OpenApi.Readers.Services +{ + /// + /// Builds a list of all remote references used in an OpenApi document + /// + internal class OpenApiRemoteReferenceCollector : OpenApiVisitorBase + { + + public List References { get; } + } +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index d48cb9310..7c0451da0 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Writers; namespace Microsoft.OpenApi.Models @@ -16,6 +16,11 @@ namespace Microsoft.OpenApi.Models /// public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible { + /// + /// Related workspace containing OpenApiDocuments that are referenced in this document + /// + public OpenApiWorkspace Workspace { get; set; } + /// /// REQUIRED. Provides metadata about the API. The metadata MAY be used by tooling as required. /// diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs new file mode 100644 index 000000000..14eeaf620 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Contains a set of OpenApi documents that reference each other + /// + public class OpenApiWorkspace + { + /// + /// Load OpenApiDocuments and IOpenApiElements referenced + /// + /// List of remote references to load + public async Task LoadAsync(List remoteReferences) + { + //TODO: Load remote documents + return; + } + } +} From c82c3cff35a76b2241041d820a3b70c9105816d5 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 10 Feb 2018 04:14:47 -0800 Subject: [PATCH 02/32] Fixed build --- src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index 7389bca5b..33088ed30 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -151,7 +151,7 @@ internal static YamlDocument LoadYamlDocument(Stream input) return yamlDocument; } - private static Task ResolveAllReferences(OpenApiDocument document) + private static async Task ResolveAllReferences(OpenApiDocument document) { // Collect remote references by walking document var referenceCollector = new OpenApiRemoteReferenceCollector(); @@ -159,7 +159,7 @@ private static Task ResolveAllReferences(OpenApiDocument document) // Load References into a workspace var openApiWorkspace = new OpenApiWorkspace(); - openApiWorkspace.LoadAsync(referenceCollector.References).Wait(); + await openApiWorkspace.LoadAsync(referenceCollector.References); // Attach Workspace to primary document document.Workspace = openApiWorkspace; From dffad37a7967893df9959fd2036a93ef2a3168ed Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 12 Feb 2018 12:42:11 -0500 Subject: [PATCH 03/32] Skeleton code for OpenApiWorkspace --- .../Interface/IStreamLoader.cs | 14 +++ .../Microsoft.OpenApi.Readers.csproj | 4 + .../OpenApiStreamReader.cs | 50 ++++----- .../Services/DefaultStreamLoader.cs | 34 ++++++ .../OpenApiRemoteReferenceCollector.cs | 101 +++++++++++++++++- .../Services/OpenApiWorkspaceLoader.cs | 52 +++++++++ .../Services/OpenApiWorkspace.cs | 40 +++++-- 7 files changed, 255 insertions(+), 40 deletions(-) create mode 100644 src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs create mode 100644 src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs create mode 100644 src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs diff --git a/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs new file mode 100644 index 000000000..96f55cd4a --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.OpenApi.Readers.Interface +{ + public interface IStreamLoader + { + Task LoadAsync(Uri uri); + } +} diff --git a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj index 0cc80ba00..d5c2bd48d 100644 --- a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj +++ b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj @@ -33,6 +33,10 @@ + + + + True diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index f8f52ab70..0adcd8daa 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -50,7 +50,7 @@ public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) case ReferenceResolutionSetting.ResolveRemoteReferences: throw new ArgumentException("Cannot resolve remote references using synchronous Read method, use ReadAsync instead"); case ReferenceResolutionSetting.ResolveLocalReferences: - ResolveLocalReferences(document); + ResolveReferences(document,false); break; case ReferenceResolutionSetting.DoNotResolveReferences: @@ -73,30 +73,45 @@ public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) /// Will be made public once finalized. internal async Task ReadAsync(Stream input, OpenApiDiagnostic diagnostic, OpenApiWorkspace openApiWorkspace = null) { - // Parse the OpenAPI Document OpenApiDocument document = ParseStream(input, diagnostic); + // Load Document into workspace and load all referenced documents + var workspace = openApiWorkspace ?? new OpenApiWorkspace(); + var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkspace, new DefaultStreamLoader(), _settings); + await workspaceLoader.LoadAsync(null,document); + // Resolve References if requested switch (_settings.ReferenceResolution) { case ReferenceResolutionSetting.ResolveRemoteReferences: - await ResolveAllReferences(document); + // Resolve references in documents + foreach (var item in workspace.Documents) + { + ResolveReferences(item, true); + } break; case ReferenceResolutionSetting.ResolveLocalReferences: - ResolveLocalReferences(document); + // Resolve references in documents + foreach (var item in workspace.Documents) + { + ResolveReferences(item,false); + } break; case ReferenceResolutionSetting.DoNotResolveReferences: break; } - ValidateDocument(diagnostic, document); + // Validate loaded documents + foreach (var item in workspace.Documents) + { + ValidateDocument(diagnostic, item); + } return document; } - private OpenApiDocument ParseStream(Stream input, OpenApiDiagnostic diagnostic) { ParsingContext context; @@ -150,28 +165,9 @@ internal static YamlDocument LoadYamlDocument(Stream input) return yamlDocument; } - private static async Task ResolveAllReferences(OpenApiDocument document) - { - // Collect remote references by walking document - var referenceCollector = new OpenApiRemoteReferenceCollector(); - var collectorWalker = new OpenApiWalker(referenceCollector); - - // Load References into a workspace - var openApiWorkspace = new OpenApiWorkspace(); - await openApiWorkspace.LoadAsync(referenceCollector.References); - - // Attach Workspace to primary document - document.Workspace = openApiWorkspace; - - // Load all remote references into OpenApiWorkspace - var resolver = new OpenApiReferenceResolver(document, true); - var walker = new OpenApiWalker(resolver); - walker.Walk(document); - } - - private static void ResolveLocalReferences(OpenApiDocument document) + private static void ResolveReferences(OpenApiDocument document, bool includeRemote) { - var resolver = new OpenApiReferenceResolver(document, false); + var resolver = new OpenApiReferenceResolver(document, includeRemote); var walker = new OpenApiWalker(resolver); walker.Walk(document); } diff --git a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs new file mode 100644 index 000000000..d523e2bc0 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Readers.Interface; + +namespace Microsoft.OpenApi.Readers.Services +{ + internal class DefaultStreamLoader : IStreamLoader + { + private HttpClient _httpClient = new HttpClient(); + + public async Task LoadAsync(Uri uri) + { + switch (uri.Scheme) + { + case "file": + return File.OpenRead(uri.AbsolutePath); + case "http": + case "https": + return await _httpClient.GetStreamAsync(uri); + + default: + throw new ArgumentException("Unsupported scheme"); + } + } + } +} diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs index c62e48373..0b934e36d 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs @@ -1,8 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -13,7 +12,99 @@ namespace Microsoft.OpenApi.Readers.Services /// internal class OpenApiRemoteReferenceCollector : OpenApiVisitorBase { + private OpenApiDocument _document; + private Dictionary _references; + public OpenApiRemoteReferenceCollector(OpenApiDocument document) + { + _document = document; + } + + // TODO PathItem + // TODO Example + + /// + /// List of external references collected from OpenApiDocument + /// + public IEnumerable References + { + get { + return _references.Values; + } + } + + /// + /// Collect external reference + /// + public override void Visit(OpenApiParameter parameter) + { + AddReference(parameter.Reference); + } + + /// + /// Collect external reference + /// + public override void Visit(OpenApiCallback callback) + { + AddReference(callback.Reference); + + } + + /// + /// Collect external reference + /// + public override void Visit(OpenApiLink link) + { + AddReference(link.Reference); + } + + /// + /// Collect external reference + /// + public override void Visit(OpenApiRequestBody requestBody) + { + AddReference(requestBody.Reference); + } + + /// + /// Collect external reference + /// + public override void Visit(OpenApiResponse header) + { + AddReference(header.Reference); + } + + /// + /// Collect external reference + /// + public override void Visit(OpenApiHeader header) + { + AddReference(header.Reference); + } + + /// + /// Collect external reference + /// + public override void Visit(OpenApiSchema schema) + { + AddReference(schema.Reference); + } - public List References { get; } + /// + /// Collect external reference + /// + private void AddReference(OpenApiReference reference) + { + if (reference != null) + { + if (reference.IsExternal) + { + if (!_references.ContainsKey(reference.ExternalResource)) + { + _references.Add(reference.ExternalResource, reference); + } + } + } + } + } } diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs new file mode 100644 index 000000000..2a7904ae6 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Interface; +using Microsoft.OpenApi.Services; + +namespace Microsoft.OpenApi.Readers.Services +{ + internal class OpenApiWorkspaceLoader + { + private OpenApiWorkspace _workspace; + private IStreamLoader _streamLoader; + private OpenApiDiagnostic _diagnostics; + private OpenApiReaderSettings _readerSettings; + + public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, IStreamLoader streamloader, OpenApiReaderSettings readerSettings) + { + _workspace = workspace; + _streamLoader = streamloader; + _readerSettings = readerSettings; + _readerSettings.ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences; + } + + internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument document) + { + _workspace.AddDocument(reference.ExternalResource, document); + document.Workspace = _workspace; + + // Collect remote references by walking document + var referenceCollector = new OpenApiRemoteReferenceCollector(document); + var collectorWalker = new OpenApiWalker(referenceCollector); + collectorWalker.Walk(document); + + // Walk references + foreach (var item in referenceCollector.References) + { + // If not already in workspace, load it and process references + if (!_workspace.Contains(item.ExternalResource)) + { + var stream = await _streamLoader.LoadAsync(new Uri(item.ExternalResource)); + var reader = new OpenApiStreamReader(_readerSettings); + var newDocument = reader.Read(stream, out _diagnostics); + await LoadAsync(item, newDocument); + } + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 14eeaf620..6987c789d 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -6,23 +6,47 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; namespace Microsoft.OpenApi.Services { /// - /// Contains a set of OpenApi documents that reference each other + /// Contains a set of OpenApi documents and document fragments that reference each other /// public class OpenApiWorkspace { - /// - /// Load OpenApiDocuments and IOpenApiElements referenced - /// - /// List of remote references to load - public async Task LoadAsync(List remoteReferences) + + public IEnumerable Documents { get; } + + public IEnumerable Fragments { get; } + + + public bool Contains(string location) + { + return true; + } + public void AddDocument(string location, OpenApiDocument document) + { + + } + + public void AddFragment(string location, IOpenApiFragment fragment) + { + + } + + public IOpenApiReferenceable ResolveReference(OpenApiReference reference) { - //TODO: Load remote documents - return; + // Find the doc/fragment + // Call ResolveReference on it + return null; } + + } + + public interface IOpenApiFragment + { + IOpenApiReferenceable ResolveReference(OpenApiReference reference); } } From 3d37908ea674a63c708b6b5a0279a3a7eb76c319 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 12 Feb 2018 23:04:32 -0500 Subject: [PATCH 04/32] Added comment --- src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs index d523e2bc0..f66915642 100644 --- a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs @@ -12,6 +12,9 @@ namespace Microsoft.OpenApi.Readers.Services { + /// + /// Implementation of that loads streams from URIs + /// internal class DefaultStreamLoader : IStreamLoader { private HttpClient _httpClient = new HttpClient(); From 60ef50e005f8a5cca4ae4c76db901d39648ec75c Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 18 Mar 2018 13:25:02 -0400 Subject: [PATCH 05/32] Workspace reference --- .../Interface/IStreamLoader.cs | 23 ++- .../OpenApiStreamReader.cs | 10 +- .../Services/DefaultStreamLoader.cs | 4 +- .../Services/OpenApiReferenceResolver.cs | 7 +- .../Services/OpenApiWorkspaceLoader.cs | 20 +- .../Models/OpenApiDocument.cs | 4 +- .../Services/OpenApiWorkspace.cs | 21 +- .../OpenApiWorkspaceStreamTests.cs | 12 ++ .../Workspaces/OpenApiWorkspaceTests.cs | 189 ++++++++++++++++++ 9 files changed, 255 insertions(+), 35 deletions(-) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs diff --git a/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs index 96f55cd4a..191f84761 100644 --- a/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs @@ -1,14 +1,23 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; using System.Threading.Tasks; +using Microsoft.OpenApi.Models; namespace Microsoft.OpenApi.Readers.Interface { - public interface IStreamLoader + /// + /// Interface for service that translates a URI into an object that can be loaded by a Reader + /// + /// + public interface IInputLoader { - Task LoadAsync(Uri uri); + /// + /// Use Uri to locate data and convert into an input object. + /// + /// Identifier of some source of an OpenAPI Description + /// A data objext that can be processed by a reader to generate an + Task LoadAsync(Uri uri); } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index d6171a7b3..c806cb891 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -78,7 +78,15 @@ internal async Task ReadAsync(Stream input, OpenApiDiagnostic d // Load Document into workspace and load all referenced documents var workspace = openApiWorkspace ?? new OpenApiWorkspace(); - var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkspace, new DefaultStreamLoader(), _settings); + var settings = new OpenApiReaderSettings() + { + ExtensionParsers = _settings.ExtensionParsers, + RuleSet = _settings.RuleSet, + ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences + }; + + var reader = new OpenApiStreamReader(settings); + var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkspace, new DefaultStreamLoader(), reader); await workspaceLoader.LoadAsync(null,document); // Resolve References if requested diff --git a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs index f66915642..311642a82 100644 --- a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs @@ -13,9 +13,9 @@ namespace Microsoft.OpenApi.Readers.Services { /// - /// Implementation of that loads streams from URIs + /// Implementation of IInputLoader that loads streams from URIs /// - internal class DefaultStreamLoader : IStreamLoader + internal class DefaultStreamLoader : IInputLoader { private HttpClient _httpClient = new HttpClient(); diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs index 97965d202..eb0ffad58 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs @@ -180,12 +180,7 @@ private void ResolveTags(IList tags) } else if (_resolveRemoteReferences == true) { - // TODO: Resolve Remote reference - return new T() - { - UnresolvedReference = true, - Reference = reference - }; + return _currentDocument.Workspace.ResolveReference(reference) as T; } else { diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs index 2a7904ae6..42fffb523 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -10,19 +10,18 @@ namespace Microsoft.OpenApi.Readers.Services { - internal class OpenApiWorkspaceLoader + internal class OpenApiWorkspaceLoader where TDiagnostic: IDiagnostic { private OpenApiWorkspace _workspace; - private IStreamLoader _streamLoader; - private OpenApiDiagnostic _diagnostics; - private OpenApiReaderSettings _readerSettings; + private IInputLoader _loader; + private TDiagnostic _diagnostics; + private IOpenApiReader _reader; - public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, IStreamLoader streamloader, OpenApiReaderSettings readerSettings) + public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, IInputLoader loader, IOpenApiReader reader) { _workspace = workspace; - _streamLoader = streamloader; - _readerSettings = readerSettings; - _readerSettings.ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences; + _loader = loader; + _reader = reader; } internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument document) @@ -41,9 +40,8 @@ internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument docume // If not already in workspace, load it and process references if (!_workspace.Contains(item.ExternalResource)) { - var stream = await _streamLoader.LoadAsync(new Uri(item.ExternalResource)); - var reader = new OpenApiStreamReader(_readerSettings); - var newDocument = reader.Read(stream, out _diagnostics); + var input = await _loader.LoadAsync(new Uri(item.ExternalResource)); + var newDocument = _reader.Read(input, out _diagnostics); await LoadAsync(item, newDocument); } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index b8e0d74b9..a020567c4 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -274,14 +274,14 @@ private static void WriteHostInfoV2(IOpenApiWriter writer, IList /// /// Load the referenced object from a object /// - public IOpenApiReferenceable ResolveReference(OpenApiReference reference) + public IOpenApiReferenceable ResolveReference(OpenApiReference reference, bool useExternal = false) { if (reference == null) { return null; } - if (reference.IsExternal) + if (reference.IsExternal && !useExternal) { // Should not attempt to resolve external references against a single document. throw new ArgumentException(Properties.SRResource.RemoteReferenceNotSupported); diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 6987c789d..3a4aed223 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -16,8 +16,13 @@ namespace Microsoft.OpenApi.Services /// public class OpenApiWorkspace { + private Dictionary _documents = new Dictionary(); - public IEnumerable Documents { get; } + public IEnumerable Documents { + get { + return _documents.Values; + } + } public IEnumerable Fragments { get; } @@ -26,9 +31,10 @@ public bool Contains(string location) { return true; } - public void AddDocument(string location, OpenApiDocument document) - { + public void AddDocument(string location, OpenApiDocument document) + { + _documents.Add(location, document); } public void AddFragment(string location, IOpenApiFragment fragment) @@ -38,9 +44,12 @@ public void AddFragment(string location, IOpenApiFragment fragment) public IOpenApiReferenceable ResolveReference(OpenApiReference reference) { - // Find the doc/fragment - // Call ResolveReference on it - return null; + if (!_documents.TryGetValue(reference.ExternalResource,out var doc)) + { + return null; + } + + return doc.ResolveReference(reference,true); } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs new file mode 100644 index 000000000..3893310c2 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.OpenApi.Readers.Tests.OpenApiWorkspaceTests +{ + class OpenApiWorkspaceStreamTests + { + } +} diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs new file mode 100644 index 000000000..d7af2d1c1 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; + +namespace Microsoft.OpenApi.Tests +{ + + public class OpenApiWorkspaceTests + { + [Fact] + public void OpenApiWorkspaceCanHoldMultipleDocuments() + { + var workspace = new OpenApiWorkspace(); + + workspace.AddDocument("root", new OpenApiDocument()); + workspace.AddDocument("common", new OpenApiDocument()); + + Assert.Equal(2, workspace.Documents.Count()); + } + + [Fact] + public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther() + { + var workspace = new OpenApiWorkspace(); + + workspace.AddDocument("root", new OpenApiDocument() { + Paths = new OpenApiPaths() + { + ["/"] = new OpenApiPathItem() + { + Operations = new Dictionary() + { + [OperationType.Get] = new OpenApiOperation() { + Responses = new OpenApiResponses() + { + ["200"] = new OpenApiResponse() + { + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema() + { + Reference = new OpenApiReference() + { + Id = "test", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + } + } + } + }); + workspace.AddDocument("common", new OpenApiDocument() { + Components = new OpenApiComponents() + { + Schemas = { + ["test"] = new OpenApiSchema() { + Type = "string", + Description = "The referenced one" + } + } + } + }); + + Assert.Equal(2, workspace.Documents.Count()); + } + + [Fact] + public void OpenApiWorkspacesCanResolveExternalReferences() + { + var workspace = new OpenApiWorkspace(); + workspace.AddDocument("common", CreateCommonDocument()); + var schema = workspace.ResolveReference(new OpenApiReference() + { + Id = "test", + Type = ReferenceType.Schema, + ExternalResource ="common" + }) as OpenApiSchema; + + Assert.NotNull(schema); + Assert.Equal("The referenced one", schema.Description); + } + + [Fact] + public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short() + { + var workspace = new OpenApiWorkspace(); + + var doc = new OpenApiDocument(); + doc.CreatePathItem("/", p => + { + p.Description = "Consumer"; + p.CreateOperation(OperationType.Get, op => + op.CreateResponse("200", re => + { + re.Description = "Success"; + re.CreateContent("application/json", co => + co.Schema = new OpenApiSchema() + { + Reference = new OpenApiReference() + { + Id = "test", + Type = ReferenceType.Schema + }, + UnresolvedReference = true + } + ); + }) + ); + }); + + workspace.AddDocument("root", doc); + + workspace.AddDocument("common", CreateCommonDocument()); + + var schema = doc.Paths["/"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema; + Assert.True(schema.UnresolvedReference); + } + + private static OpenApiDocument CreateCommonDocument() + { + return new OpenApiDocument() + { + Components = new OpenApiComponents() + { + Schemas = { + ["test"] = new OpenApiSchema() { + Type = "string", + Description = "The referenced one" + } + } + } + }; + } + } + + + + + public static class OpenApiFactoryExtensions { + + public static OpenApiDocument CreatePathItem(this OpenApiDocument document, string path, Action config) + { + var pathItem = new OpenApiPathItem(); + config(pathItem); + document.Paths.Add(path, pathItem); + return document; + } + + public static OpenApiPathItem CreateOperation(this OpenApiPathItem parent, OperationType opType, Action config) + { + var child = new OpenApiOperation(); + config(child); + parent.Operations.Add(opType, child); + return parent; + } + + public static OpenApiOperation CreateResponse(this OpenApiOperation parent, string status, Action config) + { + var child = new OpenApiResponse(); + config(child); + parent.Responses.Add(status, child); + return parent; + } + + public static OpenApiResponse CreateContent(this OpenApiResponse parent, string mediaType, Action config) + { + var child = new OpenApiMediaType(); + config(child); + parent.Content.Add(mediaType, child); + return parent; + } + +} +} From a5667a0f1260f9af22a08de8489649b139b050a3 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 31 Mar 2018 10:10:19 -0400 Subject: [PATCH 06/32] Who knows what is in here! --- .../Workspaces/OpenApiWorkspaceTests.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index d7af2d1c1..383d61c33 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -111,10 +111,11 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short() re.CreateContent("application/json", co => co.Schema = new OpenApiSchema() { - Reference = new OpenApiReference() + Reference = new OpenApiReference() // Reference { Id = "test", - Type = ReferenceType.Schema + Type = ReferenceType.Schema, + ExternalResource = "common" }, UnresolvedReference = true } @@ -127,6 +128,8 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short() workspace.AddDocument("common", CreateCommonDocument()); + + var schema = doc.Paths["/"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema; Assert.True(schema.UnresolvedReference); } From e5893b69e346954fefef6b6dbb2cdca34f98910e Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 8 Apr 2018 17:34:36 -0400 Subject: [PATCH 07/32] Moved reference resolution into the OpenApi model project --- .../OpenApiStreamReader.cs | 15 +++------------ .../Extensions/OpenApiElementExtensions.cs | 1 + src/Microsoft.OpenApi/Models/OpenApiDocument.cs | 11 +++++++++++ .../Services/OpenApiReferenceResolver.cs | 2 +- .../Services/OpenApiWorkspace.cs | 6 ++++++ 5 files changed, 22 insertions(+), 13 deletions(-) rename src/{Microsoft.OpenApi.Readers => Microsoft.OpenApi}/Services/OpenApiReferenceResolver.cs (99%) diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index 965af3eb4..7ab3defb9 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -51,7 +51,7 @@ public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) case ReferenceResolutionSetting.ResolveAllReferences: throw new ArgumentException("Cannot resolve remote references using synchronous Read method, use ReadAsync instead"); case ReferenceResolutionSetting.ResolveLocalReferences: - ResolveReferences(document,false); + document.ResolveReferences(false); break; case ReferenceResolutionSetting.DoNotResolveReferences: @@ -97,14 +97,14 @@ internal async Task ReadAsync(Stream input, OpenApiDiagnostic d // Resolve references in documents foreach (var item in workspace.Documents) { - ResolveReferences(item, true); + item.ResolveReferences(true); } break; case ReferenceResolutionSetting.ResolveLocalReferences: // Resolve references in documents foreach (var item in workspace.Documents) { - ResolveReferences(item,false); + item.ResolveReferences(false); } break; case @@ -156,7 +156,6 @@ private void ValidateDocument(OpenApiDiagnostic diagnostic, OpenApiDocument docu } } - /// /// Helper method to turn streams into YamlDocument /// @@ -173,13 +172,5 @@ internal static YamlDocument LoadYamlDocument(Stream input) } return yamlDocument; } - - private static void ResolveReferences(OpenApiDocument document, bool includeRemote) - { - var resolver = new OpenApiReferenceResolver(document, includeRemote); - var walker = new OpenApiWalker(resolver); - walker.Walk(document); - } - } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs index 5697a71ab..547a66c1e 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs @@ -27,5 +27,6 @@ public static IEnumerable Validate(this IOpenApiElement element, V walker.Walk(element); return validator.Errors; } + } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index a020567c4..2d898850d 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -271,6 +271,17 @@ private static void WriteHostInfoV2(IOpenApiWriter writer, IList writer.WriteOptionalCollection(OpenApiConstants.Schemes, schemes, (w, s) => w.WriteValue(s)); } + /// + /// Walk the OpenApiDocument and resolve unresolved references + /// + /// Indicates if external references should be resolved. Document needs to reference a workspace for this to be possible. + public void ResolveReferences(bool useExternal = false) + { + var resolver = new OpenApiReferenceResolver(this, useExternal); + var walker = new OpenApiWalker(resolver); + walker.Walk(this); + } + /// /// Load the referenced object from a object /// diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs b/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs similarity index 99% rename from src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs rename to src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs index 24d7161f2..12eae68f3 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiReferenceResolver.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs @@ -9,7 +9,7 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; -namespace Microsoft.OpenApi.Readers.Services +namespace Microsoft.OpenApi.Services { /// /// This class is used to walk an OpenApiDocument and convert unresolved references to references to populated objects diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 3a4aed223..4dc5fd3bb 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -18,12 +18,18 @@ public class OpenApiWorkspace { private Dictionary _documents = new Dictionary(); + /// + /// A list of OpenApiDocuments contained in the workspace + /// public IEnumerable Documents { get { return _documents.Values; } } + /// + /// A list of document fragments that are contained in the workspace + /// public IEnumerable Fragments { get; } From af0b5fcbc2719d50269f37a905276c49b18dab71 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Wed, 11 Apr 2018 15:32:00 -0700 Subject: [PATCH 08/32] More workspace fixes --- .../OpenApiReaderError.cs | 6 ++++ .../ParseNodes/PropertyNode.cs | 2 +- .../OpenApiRemoteReferenceCollector.cs | 2 +- .../OpenApiSecurityRequirementDeserializer.cs | 2 +- .../OpenApiSecurityRequirementDeserializer.cs | 2 +- .../Extensions/OpenApiElementExtensions.cs | 1 - src/Microsoft.OpenApi/Models/OpenApiError.cs | 2 +- .../Services/OpenApiReferenceError.cs | 29 +++++++++++++++++++ .../Services/OpenApiReferenceResolver.cs | 18 ++++++++++-- .../Services/OpenApiWorkspace.cs | 8 ++++- .../Workspaces/OpenApiWorkspaceTests.cs | 17 +++++++++-- 11 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs index b06a01f31..8d30b3113 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs @@ -27,6 +27,12 @@ public OpenApiReaderError(OpenApiException exception) : base(exception) } + public OpenApiReaderError(ParsingContext context, string message) : base(context.GetLocation(), message) + { + + } + + /// /// Create error object from YAML SyntaxErrorException /// diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs index 1f4c9adcb..0714d829a 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs @@ -73,7 +73,7 @@ public void ParseField( else { Diagnostic.Errors.Add( - new OpenApiError("", $"{Name} is not a valid property at {Context.GetLocation()}")); + new OpenApiReaderError(Context, $"{Name} is not a valid property")); } } } diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs index 0b934e36d..275fbf7c0 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs @@ -13,7 +13,7 @@ namespace Microsoft.OpenApi.Readers.Services internal class OpenApiRemoteReferenceCollector : OpenApiVisitorBase { private OpenApiDocument _document; - private Dictionary _references; + private Dictionary _references = new Dictionary(); public OpenApiRemoteReferenceCollector(OpenApiDocument document) { _document = document; diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs index d214a9b11..2293c046a 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs @@ -34,7 +34,7 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) else { node.Diagnostic.Errors.Add( - new OpenApiError(node.Context.GetLocation(), + new OpenApiReaderError(node.Context, $"Scheme {property.Name} is not found")); } } diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs index b8c3c38bb..2567c7826 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs @@ -34,7 +34,7 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) else { node.Diagnostic.Errors.Add( - new OpenApiError(node.Context.GetLocation(), $"Scheme {property.Name} is not found")); + new OpenApiReaderError(node.Context, $"Scheme {property.Name} is not found")); } } diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs index 547a66c1e..5697a71ab 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs @@ -27,6 +27,5 @@ public static IEnumerable Validate(this IOpenApiElement element, V walker.Walk(element); return validator.Errors; } - } } \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Models/OpenApiError.cs b/src/Microsoft.OpenApi/Models/OpenApiError.cs index 2b6d85635..f58001050 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiError.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiError.cs @@ -8,7 +8,7 @@ namespace Microsoft.OpenApi.Models /// /// Error related to the Open API Document. /// - public class OpenApiError + public abstract class OpenApiError { /// /// Initializes the class using the message and pointer from the given exception. diff --git a/src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs b/src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs new file mode 100644 index 000000000..a35cfcb31 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + public class OpenApiReferenceError : OpenApiError + { + private OpenApiReference _reference; + /// + /// Initializes the class using the message and pointer from the given exception. + /// + public OpenApiReferenceError(OpenApiException exception) : base(exception.Pointer, exception.Message) + { + } + + public OpenApiReferenceError(OpenApiReference reference, string message) : base("", message) + { + _reference = reference; + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs b/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs index 12eae68f3..53716347c 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs @@ -217,13 +217,27 @@ private void ResolveTags(IList tags) } catch (OpenApiException ex) { - _errors.Add(new OpenApiError(ex)); + _errors.Add(new OpenApiReferenceError(ex)); return null; } } else if (_resolveRemoteReferences == true) { - return _currentDocument.Workspace.ResolveReference(reference) as T; + if (_currentDocument.Workspace == null) + { + _errors.Add(new OpenApiReferenceError(reference,"Cannot resolve external references for documents not in workspaces.")); + // Leave as unresolved reference + return new T() + { + UnresolvedReference = true, + Reference = reference + }; + } + var target = _currentDocument.Workspace.ResolveReference(reference); + + // TODO: If it is a document fragment, then we should resolve it within the current context + + return target as T; } else { diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 4dc5fd3bb..1659c5e93 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -35,11 +35,12 @@ public IEnumerable Documents { public bool Contains(string location) { - return true; + return _documents.ContainsKey(location); } public void AddDocument(string location, OpenApiDocument document) { + document.Workspace = this; _documents.Add(location, document); } @@ -48,6 +49,11 @@ public void AddFragment(string location, IOpenApiFragment fragment) } + public void AddArtifact(string location, T artifact) + { + + } + public IOpenApiReferenceable ResolveReference(OpenApiReference reference) { if (!_documents.TryGetValue(reference.ExternalResource,out var doc)) diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index 383d61c33..b8a1bf79c 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -128,12 +128,25 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short() workspace.AddDocument("common", CreateCommonDocument()); - + doc.ResolveReferences(true); var schema = doc.Paths["/"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema; - Assert.True(schema.UnresolvedReference); + Assert.False(schema.UnresolvedReference); } + + [Fact] + public void OpenApiWorkspacesShouldNormalizeDocumentLocations() + { + Assert.True(false); + } + + // Enable Workspace to load from any reader, not just streams. + + // Test fragments + + // Test artifacts + private static OpenApiDocument CreateCommonDocument() { return new OpenApiDocument() From 806c067b55fc0cf3eda900fab9d691e630d1cc4d Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 20 May 2018 19:02:48 -0400 Subject: [PATCH 09/32] Fixed merge errors --- src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index 8096fb8e2..a0d683e98 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -153,9 +153,11 @@ private void ValidateDocument(OpenApiDiagnostic diagnostic, OpenApiDocument docu if (_settings.RuleSet != null && _settings.RuleSet.Rules.Count > 0) { var errors = document.Validate(_settings.RuleSet); - diagnostic.Errors.Add(errors); + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } } - return document; } /// @@ -196,7 +198,7 @@ public T ReadFragment(Stream input, OpenApiSpecVersion version, out OpenApiDi } catch (OpenApiException ex) { - diagnostic.Errors.Add(new OpenApiError(ex)); + diagnostic.Errors.Add(new OpenApiReaderError(ex)); } // Validate the element From 78e7eeee9d0635ec67e0b710198e8e4b151fb7a7 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 21 May 2018 15:53:02 -0700 Subject: [PATCH 10/32] Work on baseURL for workspace --- .../Services/OpenApiWorkspace.cs | 41 ++++++++++++------- .../Workspaces/OpenApiWorkspaceTests.cs | 18 ++++++++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 1659c5e93..94f579201 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -16,7 +17,9 @@ namespace Microsoft.OpenApi.Services /// public class OpenApiWorkspace { - private Dictionary _documents = new Dictionary(); + private Dictionary _documents = new Dictionary(); + private Dictionary _fragments = new Dictionary(); + private Dictionary _artifacts = new Dictionary(); /// /// A list of OpenApiDocuments contained in the workspace @@ -30,33 +33,42 @@ public IEnumerable Documents { /// /// A list of document fragments that are contained in the workspace /// - public IEnumerable Fragments { get; } + public IEnumerable Fragments { get; } + /// + /// The base location from where all relative references are resolved + /// + public Uri BaseUrl { get; } + + /// + /// A list of document fragments that are contained in the workspace + /// + public IEnumerable Artifacts { get; } public bool Contains(string location) { - return _documents.ContainsKey(location); + return _documents.ContainsKey(ToLocationUrl(location)); } - public void AddDocument(string location, OpenApiDocument document) + public void AddDocument(string location, OpenApiDocument document) { document.Workspace = this; - _documents.Add(location, document); + _documents.Add(ToLocationUrl(location), document); } - public void AddFragment(string location, IOpenApiFragment fragment) + public void AddFragment(string location, IOpenApiElement fragment) { - + _fragments.Add(ToLocationUrl(location), fragment); } - public void AddArtifact(string location, T artifact) + public void AddArtifact(string location, Stream artifact) { - + _artifacts.Add(ToLocationUrl(location), artifact); } public IOpenApiReferenceable ResolveReference(OpenApiReference reference) { - if (!_documents.TryGetValue(reference.ExternalResource,out var doc)) + if (!_documents.TryGetValue(new Uri(reference.ExternalResource,UriKind.RelativeOrAbsolute),out var doc)) { return null; } @@ -64,10 +76,9 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference) return doc.ResolveReference(reference,true); } - } - - public interface IOpenApiFragment - { - IOpenApiReferenceable ResolveReference(OpenApiReference reference); + private Uri ToLocationUrl(string location) + { + return new Uri(BaseUrl, location); + } } } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index b8a1bf79c..f9314c9dc 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -138,12 +138,30 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short() [Fact] public void OpenApiWorkspacesShouldNormalizeDocumentLocations() { + // what does normalize mean? + // If we use Urls as locators then normalization happens automatically. + + // How do we set a base location for a workspace? + // A base could be a folder. Should we use file:// + // A base could be a root url + // Are absolute locations allowed? + // Can a base URI change once a workspace has been created? + // What should be the default base URL? + // Can we infer it from a root document? + // Is the root document the first document loaded? + // Can we load multiple APIs into a Workspace? Does root document make sense? + // What data type should "location" really be? Is it a Uri? + // Assert.True(false); } // Enable Workspace to load from any reader, not just streams. // Test fragments + public void OpenApiWorkspacesShouldLoadDocumentFragments() + { + Assert.True(false); + } // Test artifacts From f64e23e89ea7b1796f099d3585ce7487841b8bc8 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 29 Oct 2018 09:23:17 -0400 Subject: [PATCH 11/32] Fixed location normalization --- .../Services/OpenApiWorkspace.cs | 79 +++++++++++++++++-- .../GraphTests.cs | 70 ++++++++++++++++ .../Workspaces/OpenApiWorkspaceTests.cs | 31 +++----- 3 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 test/Microsoft.OpenApi.SmokeTests/GraphTests.cs diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 94f579201..637187051 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -45,35 +45,102 @@ public IEnumerable Documents { /// public IEnumerable Artifacts { get; } + /// + /// Initialize workspace pointing to a base URL to allow resolving relative document locations. Use a file:// url to point to a folder + /// + /// + public OpenApiWorkspace(Uri baseUrl) + { + BaseUrl = baseUrl; + } + + /// + /// Initialize workspace using current directory as the default location. + /// + public OpenApiWorkspace() + { + BaseUrl = new Uri("file://" + Environment.CurrentDirectory + "\\" ); + } + + /// + /// Verify if workspace contains a document based on its URL. + /// + /// A relative or absolute URL of the file. Use file:// for folder locations. + /// Returns true if a matching document is found. public bool Contains(string location) { - return _documents.ContainsKey(ToLocationUrl(location)); + Uri key = ToLocationUrl(location); + return _documents.ContainsKey(key) || _fragments.ContainsKey(key) || _artifacts.ContainsKey(key); } + /// + /// Add an OpenApiDocument to the workspace. + /// + /// + /// public void AddDocument(string location, OpenApiDocument document) { document.Workspace = this; _documents.Add(ToLocationUrl(location), document); } - public void AddFragment(string location, IOpenApiElement fragment) + /// + /// Adds a fragment of an OpenApiDocument to the workspace. + /// + /// + /// + /// Not sure how this is going to work. Does the reference just point to the fragment as a whole, or do we need to + /// to be able to point into the fragment. Keeping it private until we figure it out. + /// + private void AddFragment(string location, IOpenApiElement fragment) { _fragments.Add(ToLocationUrl(location), fragment); } + /// + /// Add a stream based artificat to the workspace. Useful for images, examples, alternative schemas. + /// + /// + /// public void AddArtifact(string location, Stream artifact) { _artifacts.Add(ToLocationUrl(location), artifact); } + /// + /// Returns the target of an OpenApiReference from within the workspace. + /// + /// An instance of an OpenApiReference + /// public IOpenApiReferenceable ResolveReference(OpenApiReference reference) { - if (!_documents.TryGetValue(new Uri(reference.ExternalResource,UriKind.RelativeOrAbsolute),out var doc)) + if (_documents.TryGetValue(new Uri(BaseUrl,reference.ExternalResource),out var doc)) { - return null; + return doc.ResolveReference(reference, true); } - - return doc.ResolveReference(reference,true); + else if (_fragments.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var fragment)) + { + var frag = fragment as IOpenApiReferenceable; + if (frag != null) + { + return null; // frag.ResolveReference(reference, true); // IOpenApiElement needs to implement ResolveReference + } + else + { + return null; + } + } + return null; + } + + /// + /// + /// + /// + /// + public Stream GetArtifact(string location) + { + return _artifacts[ToLocationUrl(location)]; } private Uri ToLocationUrl(string location) diff --git a/test/Microsoft.OpenApi.SmokeTests/GraphTests.cs b/test/Microsoft.OpenApi.SmokeTests/GraphTests.cs new file mode 100644 index 000000000..232a37fa9 --- /dev/null +++ b/test/Microsoft.OpenApi.SmokeTests/GraphTests.cs @@ -0,0 +1,70 @@ +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using Microsoft.OpenApi.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.SmokeTests +{ + public class GraphTests + { + OpenApiDocument _graphOpenApi; + HttpClient _httpClient; + private readonly ITestOutputHelper _output; + const string graphOpenApiUrl = "https://github.com/microsoftgraph/microsoft-graph-openapi/blob/master/v1.0.json?raw=true"; + + public GraphTests(ITestOutputHelper output) + { + _output = output; + System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + _httpClient = new HttpClient(new HttpClientHandler() + { AutomaticDecompression = DecompressionMethods.GZip + }); + _httpClient.DefaultRequestHeaders.AcceptEncoding.Add(new System.Net.Http.Headers.StringWithQualityHeaderValue("gzip")); + _httpClient.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("OpenApi.Net.Tests", "1.0")); + + var response = _httpClient.GetAsync(graphOpenApiUrl) + .GetAwaiter().GetResult(); + + if (!response.IsSuccessStatusCode) + { + _output.WriteLine($"Couldn't load graph openapi"); + return; + } + + var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); ; + + var reader = new OpenApiStreamReader(); + _graphOpenApi = reader.Read(stream, out var diagnostic); + + if (diagnostic.Errors.Count > 0) + { + _output.WriteLine($"Errors parsing"); + _output.WriteLine(String.Join("\n", diagnostic.Errors)); + // Assert.True(false); // Uncomment to identify descriptions with errors. + } + + + } + + [Fact] + public void LoadOpen() + { + var operations = new[] { "foo","bar" }; + var workspace = new OpenApiWorkspace(); + workspace.AddDocument(graphOpenApiUrl, _graphOpenApi); + var subset = new OpenApiDocument(); + workspace.AddDocument("subset", subset); + subset. + + Assert.NotNull(_graphOpenApi); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index f9314c9dc..7b3124193 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -125,34 +125,25 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short() }); workspace.AddDocument("root", doc); - workspace.AddDocument("common", CreateCommonDocument()); - doc.ResolveReferences(true); var schema = doc.Paths["/"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema; Assert.False(schema.UnresolvedReference); } - [Fact] public void OpenApiWorkspacesShouldNormalizeDocumentLocations() { - // what does normalize mean? - // If we use Urls as locators then normalization happens automatically. - - // How do we set a base location for a workspace? - // A base could be a folder. Should we use file:// - // A base could be a root url - // Are absolute locations allowed? - // Can a base URI change once a workspace has been created? - // What should be the default base URL? - // Can we infer it from a root document? - // Is the root document the first document loaded? - // Can we load multiple APIs into a Workspace? Does root document make sense? - // What data type should "location" really be? Is it a Uri? - // - Assert.True(false); + var workspace = new OpenApiWorkspace(); + workspace.AddDocument("hello", new OpenApiDocument()); + workspace.AddDocument("hi", new OpenApiDocument()); + + Assert.True(workspace.Contains("./hello")); + Assert.True(workspace.Contains("./foo/../hello")); + Assert.True(workspace.Contains("file://" + Environment.CurrentDirectory + "/./foo/../hello")); + + Assert.False(workspace.Contains("./goodbye")); } // Enable Workspace to load from any reader, not just streams. @@ -163,6 +154,7 @@ public void OpenApiWorkspacesShouldLoadDocumentFragments() Assert.True(false); } + // Test artifacts private static OpenApiDocument CreateCommonDocument() @@ -182,9 +174,6 @@ private static OpenApiDocument CreateCommonDocument() } } - - - public static class OpenApiFactoryExtensions { public static OpenApiDocument CreatePathItem(this OpenApiDocument document, string path, Action config) From a09875d4bfe75d7895a4a9b366562ec34872c7b1 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 17 May 2020 11:26:55 -0400 Subject: [PATCH 12/32] Disabled smoke tests, all tests pass --- test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs | 4 ++-- test/Microsoft.OpenApi.SmokeTests/GraphTests.cs | 3 +-- .../Microsoft.OpenApi.SmokeTests.csproj | 11 +++++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs b/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs index 07de3d4e1..6dde6905f 100644 --- a/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs +++ b/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs @@ -70,8 +70,8 @@ JToken GetProp(JToken obj, string prop) } } - [Theory(DisplayName = "APIs.guru")] - [MemberData(nameof(GetSchemas))] + // [Theory(DisplayName = "APIs.guru")] + // [MemberData(nameof(GetSchemas))] public async Task EnsureThatICouldParse(string url) { var response = await _httpClient.GetAsync(url); diff --git a/test/Microsoft.OpenApi.SmokeTests/GraphTests.cs b/test/Microsoft.OpenApi.SmokeTests/GraphTests.cs index 232a37fa9..de3101e27 100644 --- a/test/Microsoft.OpenApi.SmokeTests/GraphTests.cs +++ b/test/Microsoft.OpenApi.SmokeTests/GraphTests.cs @@ -54,7 +54,7 @@ public GraphTests(ITestOutputHelper output) } - [Fact] + //[Fact(Skip="Run manually")] public void LoadOpen() { var operations = new[] { "foo","bar" }; @@ -62,7 +62,6 @@ public void LoadOpen() workspace.AddDocument(graphOpenApiUrl, _graphOpenApi); var subset = new OpenApiDocument(); workspace.AddDocument("subset", subset); - subset. Assert.NotNull(_graphOpenApi); } diff --git a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj index a71c89edf..7132450c6 100644 --- a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj +++ b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj @@ -1,4 +1,4 @@ - + net46;net461 @@ -9,10 +9,13 @@ - + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 46df7d20b52130e766da90361bd1a94350f0d814 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sun, 17 May 2020 14:12:42 -0400 Subject: [PATCH 13/32] Merged vnext --- .editorconfig | 123 + Microsoft.OpenApi.sln | 12 +- README.md | 15 +- build.cmd | 18 +- src/Directory.Build.props | 8 + .../Exceptions/OpenApiReaderException.cs | 18 +- .../OpenApiUnsupportedSpecVersionException.cs | 16 +- .../Interface/IDiagnostic.cs | 2 +- .../Interface/IOpenApiReader.cs | 2 +- .../Interface/IOpenApiVersionService.cs | 2 +- .../Microsoft.OpenApi.Readers.csproj | 6 +- .../OpenApiDiagnostic.cs | 2 +- .../OpenApiReaderError.cs | 45 - .../OpenApiReaderSettings.cs | 11 +- .../OpenApiStreamReader.cs | 193 +- .../OpenApiStringReader.cs | 20 +- .../OpenApiTextReaderReader.cs | 92 + .../OpenApiYamlDocumentReader.cs | 126 + .../ParseNodes/AnyFieldMap.cs | 11 + .../ParseNodes/AnyFieldMapParameter.cs | 40 + .../ParseNodes/AnyListFieldMap.cs | 12 + .../ParseNodes/AnyListFieldMapParameter.cs | 41 + .../ParseNodes/AnyMapFieldMap.cs | 12 + .../ParseNodes/AnyMapFieldMapParameter.cs | 49 + .../ParseNodes/FixedFieldMap.cs | 2 +- .../ParseNodes/JsonPointerExtensions.cs | 2 +- .../ParseNodes/ListNode.cs | 15 +- .../ParseNodes/MapNode.cs | 103 +- .../ParseNodes/OpenApiAnyConverter.cs | 268 ++ .../ParseNodes/ParseNode.cs | 45 +- .../ParseNodes/PatternFieldMap.cs | 2 +- .../ParseNodes/PropertyNode.cs | 26 +- .../ParseNodes/RootNode.cs | 9 +- .../ParseNodes/ValueNode.cs | 67 +- .../ParsingContext.cs | 88 +- .../Properties/AssemblyInfo.cs | 2 +- .../V2/OpenApiContactDeserializer.cs | 4 +- .../V2/OpenApiDocumentDeserializer.cs | 148 +- .../V2/OpenApiExternalDocsDeserializer.cs | 2 +- .../V2/OpenApiHeaderDeserializer.cs | 49 +- .../V2/OpenApiInfoDeserializer.cs | 4 +- .../V2/OpenApiLicenseDeserializer.cs | 4 +- .../V2/OpenApiOperationDeserializer.cs | 25 +- .../V2/OpenApiParameterDeserializer.cs | 85 +- .../V2/OpenApiPathItemDeserializer.cs | 4 +- .../V2/OpenApiPathsDeserializer.cs | 4 +- .../V2/OpenApiResponseDeserializer.cs | 80 +- .../V2/OpenApiSchemaDeserializer.cs | 63 +- .../OpenApiSecurityRequirementDeserializer.cs | 8 +- .../V2/OpenApiSecuritySchemeDeserializer.cs | 4 +- .../V2/OpenApiTagDeserializer.cs | 4 +- .../V2/OpenApiV2Deserializer.cs | 130 +- .../V2/OpenApiV2VersionService.cs | 7 +- .../V2/OpenApiXmlDeserializer.cs | 15 +- .../V2/TempStorageKeys.cs | 4 +- .../V3/OpenApiCallbackDeserializer.cs | 6 +- .../V3/OpenApiComponentsDeserializer.cs | 4 +- .../V3/OpenApiContactDeserializer.cs | 4 +- .../V3/OpenApiDiscriminatorDeserializer.cs | 2 +- .../V3/OpenApiDocumentDeserializer.cs | 16 +- .../V3/OpenApiEncodingDeserializer.cs | 4 +- .../V3/OpenApiExampleDeserializer.cs | 4 +- .../V3/OpenApiExternalDocsDeserializer.cs | 2 +- .../V3/OpenApiHeaderDeserializer.cs | 30 +- .../V3/OpenApiInfoDeserializer.cs | 6 +- .../V3/OpenApiLicenseDeserializer.cs | 4 +- .../V3/OpenApiLinkDeserializer.cs | 4 +- .../V3/OpenApiMediaTypeDeserializer.cs | 50 +- .../V3/OpenApiOAuthFlowDeserializer.cs | 4 +- .../V3/OpenApiOAuthFlowsDeserializer.cs | 4 +- .../V3/OpenApiOperationDeserializer.cs | 8 +- .../V3/OpenApiParameterDeserializer.cs | 46 +- .../V3/OpenApiPathItemDeserializer.cs | 4 +- .../V3/OpenApiPathsDeserializer.cs | 4 +- .../V3/OpenApiRequestBodyDeserializer.cs | 4 +- .../V3/OpenApiResponseDeserializer.cs | 6 +- .../V3/OpenApiResponsesDeserializer.cs | 4 +- .../V3/OpenApiSchemaDeserializer.cs | 71 +- .../OpenApiSecurityRequirementDeserializer.cs | 8 +- .../V3/OpenApiSecuritySchemeDeserializer.cs | 4 +- .../V3/OpenApiServerDeserializer.cs | 4 +- .../V3/OpenApiServerVariableDeserializer.cs | 4 +- .../V3/OpenApiTagDeserializer.cs | 4 +- .../V3/OpenApiV3Deserializer.cs | 127 +- .../V3/OpenApiV3VersionService.cs | 11 +- .../V3/OpenApiXmlDeserializer.cs | 4 +- src/Microsoft.OpenApi.Readers/YamlHelper.cs | 2 +- .../Microsoft.OpenApi.Tool.csproj | 25 + src/Microsoft.OpenApi.Tool/OpenApiService.cs | 81 + src/Microsoft.OpenApi.Tool/Program.cs | 30 + src/Microsoft.OpenApi.Workbench/App.xaml.cs | 2 +- src/Microsoft.OpenApi.Workbench/MainModel.cs | 37 +- .../MainWindow.xaml | 1 + .../MainWindow.xaml.cs | 2 +- .../Properties/AssemblyInfo.cs | 10 +- src/Microsoft.OpenApi/Any/AnyType.cs | 2 +- src/Microsoft.OpenApi/Any/IOpenApiAny.cs | 2 +- .../Any/IOpenApiPrimitive.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiArray.cs | 5 +- src/Microsoft.OpenApi/Any/OpenApiBinary.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiBoolean.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiByte.cs | 12 +- src/Microsoft.OpenApi/Any/OpenApiDate.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiDateTime.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiDouble.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiFloat.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiInteger.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiLong.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiNull.cs | 5 +- src/Microsoft.OpenApi/Any/OpenApiObject.cs | 5 +- src/Microsoft.OpenApi/Any/OpenApiPassword.cs | 2 +- src/Microsoft.OpenApi/Any/OpenApiPrimitive.cs | 27 +- src/Microsoft.OpenApi/Any/OpenApiString.cs | 16 +- .../Attributes/DisplayAttribute.cs | 2 +- src/Microsoft.OpenApi/Error.cs | 2 +- .../Exceptions/OpenApiException.cs | 2 +- .../Exceptions/OpenApiWriterException.cs | 2 +- .../Expressions/BodyExpression.cs | 2 +- .../Expressions/CompositeExpression.cs | 46 + .../Expressions/HeaderExpression.cs | 2 +- .../Expressions/MethodExpression.cs | 7 +- .../Expressions/RuntimeExpression.cs | 8 +- .../Expressions/StatusCodeExpression.cs | 7 +- .../Expressions/UrlExpression.cs | 9 +- .../Extensions/EnumExtensions.cs | 2 +- .../Extensions/OpenAPIWriterExtensions.cs | 26 + .../Extensions/OpenApiElementExtensions.cs | 2 +- .../Extensions/OpenApiExtensibleExtensions.cs | 5 +- .../OpenApiSerializableExtensions.cs | 16 +- .../Extensions/StringExtensions.cs | 2 +- .../Interfaces/IOpenApiElement.cs | 2 +- .../Interfaces/IOpenApiExtensible.cs | 2 +- .../Interfaces/IOpenApiExtension.cs | 3 +- .../Interfaces/IOpenApiReferenceable.cs | 4 +- .../Interfaces/IOpenApiSerializable.cs | 2 +- src/Microsoft.OpenApi/JsonPointer.cs | 2 +- .../Microsoft.OpenApi.csproj | 2 +- .../Models/OpenApiCallback.cs | 10 +- .../Models/OpenApiComponents.cs | 28 +- .../Models/OpenApiConstants.cs | 2 +- .../Models/OpenApiContact.cs | 10 +- .../Models/OpenApiDiscriminator.cs | 2 +- .../Models/OpenApiDocument.cs | 161 +- .../Models/OpenApiEncoding.cs | 4 +- src/Microsoft.OpenApi/Models/OpenApiError.cs | 6 +- .../Models/OpenApiExample.cs | 6 +- .../Models/OpenApiExtensibleDictionary.cs | 6 +- .../Models/OpenApiExternalDocs.cs | 10 +- src/Microsoft.OpenApi/Models/OpenApiHeader.cs | 12 +- src/Microsoft.OpenApi/Models/OpenApiInfo.cs | 6 +- .../Models/OpenApiLicense.cs | 10 +- src/Microsoft.OpenApi/Models/OpenApiLink.cs | 6 +- .../Models/OpenApiMediaType.cs | 4 +- .../Models/OpenApiOAuthFlow.cs | 4 +- .../Models/OpenApiOAuthFlows.cs | 4 +- .../Models/OpenApiOperation.cs | 8 +- .../Models/OpenApiParameter.cs | 41 +- .../Models/OpenApiPathItem.cs | 6 +- src/Microsoft.OpenApi/Models/OpenApiPaths.cs | 2 +- .../Models/OpenApiReference.cs | 2 +- .../Models/OpenApiRequestBody.cs | 6 +- .../Models/OpenApiResponse.cs | 43 +- .../Models/OpenApiResponses.cs | 2 +- src/Microsoft.OpenApi/Models/OpenApiSchema.cs | 47 +- .../Models/OpenApiSecurityRequirement.cs | 2 +- .../Models/OpenApiSecurityScheme.cs | 8 +- src/Microsoft.OpenApi/Models/OpenApiServer.cs | 4 +- .../Models/OpenApiServerVariable.cs | 4 +- src/Microsoft.OpenApi/Models/OpenApiTag.cs | 8 +- src/Microsoft.OpenApi/Models/OpenApiXml.cs | 10 +- src/Microsoft.OpenApi/Models/OperationType.cs | 2 +- .../Models/ParameterLocation.cs | 2 +- .../Models/ParameterStyle.cs | 2 +- src/Microsoft.OpenApi/Models/ReferenceType.cs | 2 +- .../Models/RuntimeExpressionAnyWrapper.cs | 2 +- .../Models/SecuritySchemeType.cs | 2 +- src/Microsoft.OpenApi/OpenApiFormat.cs | 2 +- .../OpenApiSerializerSettings.cs | 21 - src/Microsoft.OpenApi/OpenApiSpecVersion.cs | 2 +- .../Properties/AssemblyInfo.cs | 2 +- .../Properties/SRResource.Designer.cs | 10 + .../Properties/SRResource.resx | 3 + .../Services/ComparisonContext.cs | 87 + .../Services/LoopDetector.cs | 71 + .../Services/OpenApiAnyComparer.cs | 72 + .../Services/OpenApiComparer.cs | 21 +- .../Services/OpenApiComparerBase.cs | 263 ++ .../Services/OpenApiComparerFactory.cs | 105 + .../Services/OpenApiComponentsComparer.cs | 98 + .../Services/OpenApiContactComparer.cs | 54 + .../Services/OpenApiDictionaryComparer.cs | 88 + .../Services/OpenApiDifference.cs | 29 +- .../Services/OpenApiDifferenceOperation.cs | 19 + .../Services/OpenApiDocumentComparer.cs | 76 + .../Services/OpenApiEncodingComparer.cs | 65 + .../Services/OpenApiExampleComparer.cs | 65 + .../Services/OpenApiExternalDocsComparer.cs | 48 + .../Services/OpenApiHeaderComparer.cs | 123 + .../Services/OpenApiInfoComparer.cs | 71 + .../Services/OpenApiLicenseComparer.cs | 51 + .../Services/OpenApiMediaTypeComparer.cs | 75 + .../Services/OpenApiOAuthFlowComparer.cs | 55 + .../Services/OpenApiOAuthFlowsComparer.cs | 73 + .../Services/OpenApiOperationComparer.cs | 96 + .../Services/OpenApiOperationsComparer.cs | 88 + .../Services/OpenApiOrderedListComparer.cs | 90 + .../Services/OpenApiParameterComparer.cs | 105 + .../Services/OpenApiParametersComparer.cs | 101 + .../Services/OpenApiPathItemComparer.cs | 58 + .../Services/OpenApiPathsComparer.cs | 86 + .../Services/OpenApiReferenceComparer.cs | 72 + .../Services/OpenApiReferenceResolver.cs | 27 +- .../Services/OpenApiRequestBodyComparer.cs | 62 + .../Services/OpenApiResponseComparer.cs | 95 + .../Services/OpenApiSchemaComparer.cs | 177 ++ .../OpenApiSecurityRequirementComparer.cs | 92 + .../Services/OpenApiSecuritySchemeComparer.cs | 80 + .../Services/OpenApiServerComparer.cs | 59 + .../Services/OpenApiServerVariableComparer.cs | 54 + .../Services/OpenApiServersComparer.cs | 99 + .../Services/OpenApiTagComparer.cs | 55 + .../Services/OpenApiVisitorBase.cs | 35 +- .../Services/OpenApiWalker.cs | 207 +- .../Validations/IValidationContext.cs | 10 +- .../Validations/OpenApiValidator.cs | 28 +- .../Rules/OpenApiComponentsRules.cs | 2 +- .../Validations/Rules/OpenApiContactRules.cs | 2 +- .../Validations/Rules/OpenApiDocumentRules.cs | 2 +- .../Validations/Rules/OpenApiHeaderRules.cs | 56 + .../Validations/Rules/OpenApiInfoRules.cs | 2 +- .../Rules/OpenApiMediaTypeRules.cs | 55 + .../Rules/OpenApiOAuthFlowRules.cs | 4 +- .../Rules/OpenApiParameterRules.cs | 101 + .../Validations/Rules/OpenApiPathsRules.cs | 4 +- .../Rules/OpenApiResponsesRules.cs | 2 +- .../Validations/Rules/OpenApiSchemaRules.cs | 83 + .../Validations/Rules/RuleHelpers.cs | 247 ++ .../Validations/ValidationRule.cs | 2 +- .../Validations/ValidationRuleSet.cs | 14 +- .../Writers/FormattingStreamWriter.cs | 30 + .../Writers/IOpenApiWriter.cs | 2 +- .../Writers/OpenApiJsonWriter.cs | 16 +- .../Writers/OpenApiWriterAnyExtensions.cs | 76 +- .../Writers/OpenApiWriterBase.cs | 54 +- .../Writers/OpenApiWriterExtensions.cs | 22 +- .../Writers/OpenApiWriterSettings.cs | 36 + .../Writers/OpenApiYamlWriter.cs | 17 +- src/Microsoft.OpenApi/Writers/Scope.cs | 2 +- .../SpecialCharacterStringExtensions.cs | 18 +- .../Writers/WriterConstants.cs | 2 +- .../DefaultSettingsFixture.cs | 7 +- .../DefaultSettingsFixtureCollection.cs | 2 +- .../Microsoft.OpenApi.Readers.Tests.csproj | 105 +- .../OpenApiDiagnosticTests.cs | 4 +- .../UnsupportedSpecVersionTests.cs | 4 +- .../ParseNodeTests.cs | 35 + .../ParseNodes/OpenApiAnyConverterTests.cs | 517 ++++ .../OpenApiAnyTests.cs | 43 +- .../ConvertToOpenApiReferenceV2Tests.cs | 2 +- .../ConvertToOpenApiReferenceV3Tests.cs | 2 +- .../TryLoadReferenceV2Tests.cs | 14 +- .../TestCustomExtension.cs | 6 +- .../TestHelper.cs | 7 +- .../V2Tests/ComparisonTests.cs | 8 +- .../V2Tests/OpenApiDocumentTests.cs | 442 +++ .../V2Tests/OpenApiHeaderTests.cs | 77 + .../V2Tests/OpenApiOperationTests.cs | 78 +- .../V2Tests/OpenApiParameterTests.cs | 257 +- .../V2Tests/OpenApiPathItemTests.cs | 4 +- .../V2Tests/OpenApiSchemaTests.cs | 94 + .../V2Tests/OpenApiSecuritySchemeTests.cs | 60 +- .../V2Tests/OpenApiServerTests.cs | 295 ++ .../Samples/ComponentRootReference.json | 178 ++ .../OpenApiHeader/headerWithDefault.yaml | 3 + .../Samples/OpenApiHeader/headerWithEnum.yaml | 6 + .../operationWithResponseExamples.yaml | 16 + .../OpenApiParameter/headerParameter.yaml | 2 +- .../headerParameterWithIncorrectDataType.yaml | 16 + .../parameterWithDefault.yaml | 7 + .../OpenApiParameter/parameterWithEnum.yaml | 10 + .../parameterWithNoLocation.yaml | 4 + .../parameterWithNoSchema.yaml | 3 + .../parameterWithNullLocation.yaml | 5 + .../parameterWithUnknownLocation.yaml | 5 + .../OpenApiSchema/schemaWithDefault.yaml | 3 + .../Samples/OpenApiSchema/schemaWithEnum.yaml | 6 + .../OpenApiSchema/schemaWithExample.yaml | 3 + .../V2Tests/Samples/multipleProduces.json | 62 + .../V2Tests/Samples/twoResponses.json | 107 + .../V3Tests/OpenApiCallbackTests.cs | 157 +- .../V3Tests/OpenApiDiscriminatorTests.cs | 8 +- .../V3Tests/OpenApiDocumentTests.cs | 186 +- .../V3Tests/OpenApiEncodingTests.cs | 14 +- .../V3Tests/OpenApiExampleTests.cs | 18 +- .../V3Tests/OpenApiInfoTests.cs | 26 +- .../V3Tests/OpenApiMediaTypeTests.cs | 81 + .../V3Tests/OpenApiOperationTests.cs | 62 +- .../V3Tests/OpenApiParameterTests.cs | 349 +++ .../V3Tests/OpenApiSchemaTests.cs | 85 +- .../V3Tests/OpenApiSecuritySchemeTests.cs | 32 +- .../V3Tests/OpenApiXmlTests.cs | 8 +- ...erence.yaml => callbackWithReference.yaml} | 0 .../multipleCallbacksWithReference.yaml | 58 + .../apiWithFullHeaderComponent.yaml | 34 + .../OpenApiExample/explicitString.yaml | 36 + .../mediaTypeWithExample.yaml | 8 + .../mediaTypeWithExamples.yaml | 12 + .../operationWithParameterWithNoLocation.json | 25 + .../OpenApiParameter/headerParameter.yaml | 11 + .../parameterWithExample.yaml | 8 + .../parameterWithExamples.yaml | 12 + .../parameterWithNoLocation.yaml | 5 + .../parameterWithNullLocation.yaml | 6 + .../parameterWithUnknownLocation.yaml | 6 + .../OpenApiParameter/pathParameter.yaml | 7 + .../OpenApiParameter/queryParameter.yaml | 11 + .../queryParameterWithObjectType.yaml | 8 + ...ueryParameterWithObjectTypeAndContent.yaml | 15 + test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs | 42 +- .../DefaultSettingsFixture.cs | 7 +- .../DefaultSettingsFixtureCollection.cs | 2 +- .../Expressions/MethodExpressionTests.cs | 2 +- .../Expressions/RuntimeExpressionTests.cs | 149 +- .../Expressions/StatusCodeExpressionTests.cs | 2 +- .../Expressions/UrlExpressionTests.cs | 2 +- .../Microsoft.OpenApi.Tests.csproj | 14 +- .../Models/OpenApiCallbackTests.cs | 9 +- .../Models/OpenApiComponentsTests.cs | 10 +- .../Models/OpenApiContactTests.cs | 2 +- .../Models/OpenApiDocumentTests.cs | 437 ++- .../Models/OpenApiEncodingTests.cs | 2 +- .../Models/OpenApiExampleTests.cs | 18 +- .../Models/OpenApiExternalDocsTests.cs | 2 +- .../Models/OpenApiHeaderTests.cs | 15 +- .../Models/OpenApiInfoTests.cs | 10 +- .../Models/OpenApiLicenseTests.cs | 2 +- .../Models/OpenApiLinkTests.cs | 9 +- .../Models/OpenApiMediaTypeTests.cs | 323 ++- .../Models/OpenApiOAuthFlowTests.cs | 2 +- .../Models/OpenApiOAuthFlowsTests.cs | 2 +- .../Models/OpenApiOperationTests.cs | 12 +- .../Models/OpenApiParameterTests.cs | 131 +- .../Models/OpenApiReferenceTests.cs | 8 +- .../Models/OpenApiRequestBodyTests.cs | 9 +- .../Models/OpenApiResponseTests.cs | 38 +- .../Models/OpenApiSchemaTests.cs | 11 +- .../Models/OpenApiSecurityRequirementTests.cs | 22 +- .../Models/OpenApiSecuritySchemeTests.cs | 7 +- .../Models/OpenApiServerTests.cs | 2 +- .../Models/OpenApiServerVariableTests.cs | 2 +- .../Models/OpenApiTagTests.cs | 35 +- .../Models/OpenApiXmlTests.cs | 2 +- .../Services/OpenApiComparerTestCases.cs | 2407 +++++++++++++++++ .../Services/OpenApiComparerTests.cs | 82 + .../Services/OpenApiComponentsTests.cs | 908 +++++++ .../Services/OpenApiEncodingComparerTests.cs | 360 +++ .../Services/OpenApiExampleComparerTests.cs | 461 ++++ .../Services/OpenApiInfoComparerTests.cs | 297 ++ .../Services/OpenApiParameterComparerTests.cs | 477 ++++ .../OpenApiParametersComparerTests.cs | 432 +++ .../OpenApiRequestBodyComparerTests.cs | 587 ++++ .../Services/OpenApiResponsesComparerTests.cs | 818 ++++++ ...OpenApiSecurityRequirementComparerTests.cs | 289 ++ .../OpenApiSecuritySchemeComparerTests.cs | 308 +++ .../OpenApiServerVariableComparerTests.cs | 287 ++ .../Services/OpenApiServersComparerTests.cs | 517 ++++ .../Services/OpenApiTagComparerTests.cs | 304 +++ .../Services/OpenApiValidatorTests.cs | 45 +- .../StringExtensions.cs | 2 +- .../OpenApiComponentsValidationTests.cs | 4 +- .../OpenApiContactValidationTests.cs | 2 +- .../OpenApiExternalDocsValidationTests.cs | 2 +- .../OpenApiHeaderValidationTests.cs | 132 + .../Validations/OpenApiInfoValidationTests.cs | 6 +- .../OpenApiMediaTypeValidationTests.cs | 130 + .../OpenApiParameterValidationTests.cs | 177 ++ .../OpenApiReferenceValidationTests.cs | 14 +- .../OpenApiSchemaValidationTests.cs | 272 ++ .../Validations/OpenApiTagValidationTests.cs | 2 +- .../Validations/ValidationRuleSetTests.cs | 2 +- .../Walkers/WalkerLocationTests.cs | 155 +- .../Workspaces/OpenApiWorkspaceTests.cs | 4 +- .../Writers/OpenApiJsonWriterTests.cs | 74 +- .../OpenApiWriterAnyExtensionsTests.cs | 8 +- .../OpenApiWriterSpecialCharacterTests.cs | 8 +- .../Writers/OpenApiYamlWriterTests.cs | 365 ++- 386 files changed, 21225 insertions(+), 1663 deletions(-) create mode 100644 .editorconfig create mode 100644 src/Directory.Build.props delete mode 100644 src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs create mode 100644 src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs create mode 100644 src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs create mode 100644 src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMap.cs create mode 100644 src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMapParameter.cs create mode 100644 src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMap.cs create mode 100644 src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMapParameter.cs create mode 100644 src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMap.cs create mode 100644 src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMapParameter.cs create mode 100644 src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs create mode 100644 src/Microsoft.OpenApi.Tool/Microsoft.OpenApi.Tool.csproj create mode 100644 src/Microsoft.OpenApi.Tool/OpenApiService.cs create mode 100644 src/Microsoft.OpenApi.Tool/Program.cs create mode 100644 src/Microsoft.OpenApi/Expressions/CompositeExpression.cs create mode 100644 src/Microsoft.OpenApi/Extensions/OpenAPIWriterExtensions.cs delete mode 100644 src/Microsoft.OpenApi/OpenApiSerializerSettings.cs create mode 100644 src/Microsoft.OpenApi/Services/ComparisonContext.cs create mode 100644 src/Microsoft.OpenApi/Services/LoopDetector.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiAnyComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiComparerBase.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiComparerFactory.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiComponentsComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiContactComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiDictionaryComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiDifferenceOperation.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiDocumentComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiEncodingComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiExampleComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiExternalDocsComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiHeaderComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiInfoComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiLicenseComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiMediaTypeComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiOAuthFlowComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiOAuthFlowsComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiOperationComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiOperationsComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiOrderedListComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiParameterComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiParametersComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiPathItemComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiPathsComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiReferenceComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiRequestBodyComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiResponseComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiSchemaComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiSecurityRequirementComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiSecuritySchemeComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiServerComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiServerVariableComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiServersComparer.cs create mode 100644 src/Microsoft.OpenApi/Services/OpenApiTagComparer.cs create mode 100644 src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs create mode 100644 src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs create mode 100644 src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs create mode 100644 src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs create mode 100644 src/Microsoft.OpenApi/Writers/FormattingStreamWriter.cs create mode 100644 src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/ParseNodeTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/ParseNodes/OpenApiAnyConverterTests.cs rename test/Microsoft.OpenApi.Readers.Tests/{V3Tests => ParseNodes}/OpenApiAnyTests.cs (68%) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiHeaderTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSchemaTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiServerTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/ComponentRootReference.json create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithDefault.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithEnum.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/operationWithResponseExamples.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameterWithIncorrectDataType.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithDefault.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithEnum.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoSchema.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithDefault.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithEnum.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithExample.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/multipleProduces.json create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/twoResponses.json create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiParameterTests.cs rename test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/{advancedCallbackWithReference.yaml => callbackWithReference.yaml} (100%) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/multipleCallbacksWithReference.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/apiWithFullHeaderComponent.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiExample/explicitString.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExample.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExamples.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiOperation/operationWithParameterWithNoLocation.json create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/headerParameter.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExample.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExamples.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/pathParameter.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameter.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectType.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectTypeAndContent.yaml create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTestCases.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiComponentsTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiEncodingComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiExampleComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiInfoComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiParameterComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiParametersComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiRequestBodyComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiResponsesComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiSecurityRequirementComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiSecuritySchemeComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiServerVariableComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiServersComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Services/OpenApiTagComparerTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Validations/OpenApiMediaTypeValidationTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs create mode 100644 test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..49de0d370 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,123 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +# All files +[*] +indent_style = space +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +############################### +# VB Coding Conventions # +############################### +[*.vb] +# Modifier preferences +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion diff --git a/Microsoft.OpenApi.sln b/Microsoft.OpenApi.sln index d957905d4..e64ff3a24 100644 --- a/Microsoft.OpenApi.sln +++ b/Microsoft.OpenApi.sln @@ -1,12 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi", "src\Microsoft.OpenApi\Microsoft.OpenApi.csproj", "{A8E50143-69B2-472A-9D45-3F9A05D13202}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4AEDAD90-F854-4940-BFEE-6374CC92CAB0}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig build.cmd = build.cmd readme.md = readme.md EndProjectSection @@ -25,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6357D7FD-2 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.SmokeTests", "test\Microsoft.OpenApi.SmokeTests\Microsoft.OpenApi.SmokeTests.csproj", "{AD79B61D-88CF-497C-9ED5-41AE3867C5AC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.OpenApi.Tool", "src\Microsoft.OpenApi.Tool\Microsoft.OpenApi.Tool.csproj", "{254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,6 +58,10 @@ Global {AD79B61D-88CF-497C-9ED5-41AE3867C5AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD79B61D-88CF-497C-9ED5-41AE3867C5AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD79B61D-88CF-497C-9ED5-41AE3867C5AC}.Release|Any CPU.Build.0 = Release|Any CPU + {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -66,6 +73,7 @@ Global {AD83F991-DBF3-4251-8613-9CC54C826964} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} {1ED3C2C1-E1E7-4925-B4E6-2D969C3F5237} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} {AD79B61D-88CF-497C-9ED5-41AE3867C5AC} = {6357D7FD-2DE4-4900-ADB9-ABC37052040A} + {254841B5-7DAC-4D1D-A9C5-44FE5CE467BE} = {E546B92F-20A8-49C3-8323-4B25BB78F3E1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9F171EFC-0DB5-4B10-ABFA-AF48D52CC565} diff --git a/README.md b/README.md index 73e262ecb..bce79b6a3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ ![Category overview screenshot](docs/images/oainet.png "Microsoft + OpenAPI = Love") -# OpenAPI.NET +# OpenAPI.NET + +|Package|Nuget| +|--|--| +|Models and Writers|[![nuget](https://img.shields.io/nuget/v/Microsoft.OpenApi.svg)](https://www.nuget.org/packages/Microsoft.OpenApi/) | +|Readers | [![nuget](https://img.shields.io/nuget/v/Microsoft.OpenApi.Readers.svg)](https://www.nuget.org/packages/Microsoft.OpenApi.Readers/) | + The **OpenAPI.NET** SDK contains a useful object model for OpenAPI documents in .NET along with common serializers to extract raw OpenAPI JSON and YAML documents from the model. @@ -13,6 +19,11 @@ Project Objectives - Provide OpenAPI description writers for both V2 and V3 specification formats. - Enable developers to create Readers that translate different data formats into OpenAPI descriptions. +# Installation + +- Install core Nuget package `Microsoft.OpenApi` +- Install readers Nuget package `Microsoft.OpenApi.Readers` + # Processors The OpenAPI.NET project holds the base object model for representing OpenAPI documents as .NET objects. Some developers have found the need to write processors that convert other data formats into this OpenAPI.NET object model. We'd like to curate that list of processors in this section of the readme. @@ -20,6 +31,8 @@ The base JSON and YAML processors are built into this project. Below is the list - [**C# Comment / Annotation Processor**](https://github.com/Microsoft/OpenAPI.NET.CSharpAnnotations) : Converts standard .NET annotations ( /// comments ) emitted from your build (MSBuild.exe) into OpenAPI.NET document object. +- [**OData CSDL Processor**](https://github.com/Microsoft/OpenAPI.NET.OData) : Converts the XML representation of the Entity Data Model (EDM) describing an OData Service into OpenAPI.NET document object. + # Example Usage Creating an OpenAPI Document diff --git a/build.cmd b/build.cmd index d0d68b5d7..94f3ea8fc 100644 --- a/build.cmd +++ b/build.cmd @@ -1,21 +1,17 @@ @echo off -if "%~1"=="" goto :error - -SET %VERSION%=%~1 - -Echo Building Microsoft.OpenApi +Echo Building Microsoft.OpenApi SET PROJ=%~dp0src\Microsoft.OpenApi\Microsoft.OpenApi.csproj -msbuild %PROJ% /t:restore /p:Configuration=Release -msbuild %PROJ% /t:build /p:Configuration=Release -msbuild %PROJ% /t:pack /p:Configuration=Release;PackageOutputPath=%~dp0artifacts;Version=%VERSION% +dotnet build %PROJ% /t:restore /p:Configuration=Release +dotnet build %PROJ% /t:build /p:Configuration=Release +dotnet build %PROJ% /t:pack /p:Configuration=Release;PackageOutputPath=%~dp0artifacts Echo Building Microsoft.OpenApi.Readers SET PROJ=%~dp0src\Microsoft.OpenApi.Readers\Microsoft.OpenApi.Readers.csproj -msbuild %PROJ% /t:restore /p:Configuration=Release -msbuild %PROJ% /t:build /p:Configuration=Release -msbuild %PROJ% /t:pack /p:Configuration=Release;PackageOutputPath=%~dp0artifacts;Version=%VERSION% +dotnet build %PROJ% /t:restore /p:Configuration=Release +dotnet build %PROJ% /t:build /p:Configuration=Release +dotnet build %PROJ% /t:pack /p:Configuration=Release;PackageOutputPath=%~dp0artifacts goto :end :error diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..adb086cc1 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,8 @@ + + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + \ No newline at end of file diff --git a/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiReaderException.cs b/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiReaderException.cs index 25aac4978..03f80c93e 100644 --- a/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiReaderException.cs +++ b/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiReaderException.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using System; +using Microsoft.OpenApi.Exceptions; +using SharpYaml.Serialization; namespace Microsoft.OpenApi.Readers.Exceptions { @@ -9,7 +11,7 @@ namespace Microsoft.OpenApi.Readers.Exceptions /// Defines an exception indicating OpenAPI Reader encountered an issue while reading. /// [Serializable] - public class OpenApiReaderException : Exception + public class OpenApiReaderException : OpenApiException { /// /// Initializes the class. @@ -22,6 +24,18 @@ public OpenApiReaderException() { } /// Plain text error message for this exception. public OpenApiReaderException(string message) : base(message) { } + /// + /// Initializes the class with a message and line, column location of error. + /// + /// Plain text error message for this exception. + /// Parsing node where error occured + public OpenApiReaderException(string message, YamlNode node) : base(message) + { + // This only includes line because using a char range causes tests to break due to CR/LF & LF differences + // See https://tools.ietf.org/html/rfc5147 for syntax + Pointer = $"#line={node.Start.Line}"; + } + /// /// Initializes the class with a custom message and inner exception. /// @@ -29,4 +43,4 @@ public OpenApiReaderException(string message) : base(message) { } /// Inner exception that caused this exception to be thrown. public OpenApiReaderException(string message, Exception innerException) : base(message, innerException) { } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiUnsupportedSpecVersionException.cs b/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiUnsupportedSpecVersionException.cs index 84faf9d4e..705b212d0 100644 --- a/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiUnsupportedSpecVersionException.cs +++ b/src/Microsoft.OpenApi.Readers/Exceptions/OpenApiUnsupportedSpecVersionException.cs @@ -10,9 +10,9 @@ namespace Microsoft.OpenApi.Readers.Exceptions /// Defines an exception indicating OpenAPI Reader encountered an unsupported specification version while reading. /// [Serializable] - public class OpenApiUnsupportedSpecVersionException : OpenApiReaderException + public class OpenApiUnsupportedSpecVersionException : Exception { - const string messagePattern = "OpenAPI specification version {0} is not supported."; + const string messagePattern = "OpenAPI specification version '{0}' is not supported."; /// /// Initializes the class with a specification version. @@ -21,11 +21,6 @@ public class OpenApiUnsupportedSpecVersionException : OpenApiReaderException public OpenApiUnsupportedSpecVersionException(string specificationVersion) : base(string.Format(CultureInfo.InvariantCulture, messagePattern, specificationVersion)) { - if (string.IsNullOrWhiteSpace(specificationVersion)) - { - throw new ArgumentException("Value cannot be null or white space.", nameof(specificationVersion)); - } - this.SpecificationVersion = specificationVersion; } @@ -38,11 +33,6 @@ public OpenApiUnsupportedSpecVersionException(string specificationVersion) public OpenApiUnsupportedSpecVersionException(string specificationVersion, Exception innerException) : base(string.Format(CultureInfo.InvariantCulture, messagePattern, specificationVersion), innerException) { - if (string.IsNullOrWhiteSpace(specificationVersion)) - { - throw new ArgumentException("Value cannot be null or white space.", nameof(specificationVersion)); - } - this.SpecificationVersion = specificationVersion; } @@ -51,4 +41,4 @@ public OpenApiUnsupportedSpecVersionException(string specificationVersion, Excep /// public string SpecificationVersion { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/Interface/IDiagnostic.cs b/src/Microsoft.OpenApi.Readers/Interface/IDiagnostic.cs index 22d31e7a7..da3381f7e 100644 --- a/src/Microsoft.OpenApi.Readers/Interface/IDiagnostic.cs +++ b/src/Microsoft.OpenApi.Readers/Interface/IDiagnostic.cs @@ -9,4 +9,4 @@ namespace Microsoft.OpenApi.Readers.Interface public interface IDiagnostic { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/Interface/IOpenApiReader.cs b/src/Microsoft.OpenApi.Readers/Interface/IOpenApiReader.cs index 170cde6fb..39724b3c6 100644 --- a/src/Microsoft.OpenApi.Readers/Interface/IOpenApiReader.cs +++ b/src/Microsoft.OpenApi.Readers/Interface/IOpenApiReader.cs @@ -20,4 +20,4 @@ public interface IOpenApiReader where TDiagnostic : IDiagno /// The Open API document. OpenApiDocument Read(TInput input, out TDiagnostic diagnostic); } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/Interface/IOpenApiVersionService.cs b/src/Microsoft.OpenApi.Readers/Interface/IOpenApiVersionService.cs index 32dd420f4..a7a98d781 100644 --- a/src/Microsoft.OpenApi.Readers/Interface/IOpenApiVersionService.cs +++ b/src/Microsoft.OpenApi.Readers/Interface/IOpenApiVersionService.cs @@ -37,4 +37,4 @@ internal interface IOpenApiVersionService /// Instance of OpenApiDocument populated with data from rootNode OpenApiDocument LoadDocument(RootNode rootNode); } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj index 4df288700..368567206 100644 --- a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj +++ b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj @@ -1,4 +1,4 @@ - + net46; netstandard2.0 true @@ -10,7 +10,7 @@ Microsoft Microsoft.OpenApi.Readers Microsoft.OpenApi.Readers - 1.1.0-preview.1 + 1.2.0-preview.3 OpenAPI.NET Readers for JSON and YAML documents © Microsoft Corporation. All rights reserved. OpenAPI .NET @@ -26,7 +26,7 @@ - + diff --git a/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs b/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs index cd3258eaa..ea11c7939 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiDiagnostic.cs @@ -22,4 +22,4 @@ public class OpenApiDiagnostic : IDiagnostic /// public OpenApiSpecVersion SpecificationVersion { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs deleted file mode 100644 index 8d30b3113..000000000 --- a/src/Microsoft.OpenApi.Readers/OpenApiReaderError.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.OpenApi.Exceptions; -using Microsoft.OpenApi.Models; -using SharpYaml; - -namespace Microsoft.OpenApi.Readers -{ - /// - /// Error detected during the reading of some input and converting to an OpenApiDocument - /// - public class OpenApiReaderError : OpenApiError - { - - /// - /// Creates error object from thrown exception - /// - /// - public OpenApiReaderError(OpenApiException exception) : base(exception) - { - - } - - public OpenApiReaderError(ParsingContext context, string message) : base(context.GetLocation(), message) - { - - } - - - /// - /// Create error object from YAML SyntaxErrorException - /// - /// - public OpenApiReaderError(SyntaxErrorException exception) : base(String.Empty, exception.Message) - { - - } - } -} diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs index 544fce2fd..092699857 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; @@ -42,12 +45,16 @@ public class OpenApiReaderSettings /// /// Dictionary of parsers for converting extensions into strongly typed classes /// - public Dictionary> ExtensionParsers { get; set; } = new Dictionary>(); + public Dictionary> ExtensionParsers { get; set; } = new Dictionary>(); /// /// Rules to use for validating OpenAPI specification. If none are provided a default set of rules are applied. /// public ValidationRuleSet RuleSet { get; set; } = ValidationRuleSet.GetDefaultRuleSet(); + /// + /// URL where relative references should be resolved from if the description does not contain Server definitions + /// + public Uri BaseUrl { get; set; } } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index a0d683e98..9dc14a7bd 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -1,19 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.OpenApi.Exceptions; -using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.Interface; -using Microsoft.OpenApi.Readers.Services; -using Microsoft.OpenApi.Services; -using SharpYaml; -using SharpYaml.Serialization; namespace Microsoft.OpenApi.Readers { @@ -22,7 +13,7 @@ namespace Microsoft.OpenApi.Readers /// public class OpenApiStreamReader : IOpenApiReader { - private OpenApiReaderSettings _settings; + private readonly OpenApiReaderSettings _settings; /// /// Create stream reader with custom settings if desired. @@ -31,8 +22,8 @@ public class OpenApiStreamReader : IOpenApiReader public OpenApiStreamReader(OpenApiReaderSettings settings = null) { _settings = settings ?? new OpenApiReaderSettings(); - } + /// /// Reads the stream input and parses it into an Open API document. /// @@ -41,122 +32,9 @@ public OpenApiStreamReader(OpenApiReaderSettings settings = null) /// Instance of newly created OpenApiDocument public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) { - diagnostic = new OpenApiDiagnostic(); - - // Parse the OpenAPI Document - OpenApiDocument document = ParseStream(input, diagnostic); - - // Resolve References if requested - switch (_settings.ReferenceResolution) + using (var reader = new StreamReader(input)) { - case ReferenceResolutionSetting.ResolveAllReferences: - throw new ArgumentException("Cannot resolve remote references using synchronous Read method, use ReadAsync instead"); - case ReferenceResolutionSetting.ResolveLocalReferences: - document.ResolveReferences(false); - break; - case - ReferenceResolutionSetting.DoNotResolveReferences: - break; - } - - ValidateDocument(diagnostic, document); - - return document; - } - - /// - /// Reads the stream input and parses it into an Open API document. - /// Remote references are also loaded as OpenApiDocuments and OpenApiElements and stored in the OpenApiWorkspace - /// - /// Stream containing OpenAPI description to parse. - /// Returns diagnostic object containing errors detected during parsing. Caller must provide an instance. - /// Container for the set of OpenAPIDocuments and OpenApiElements referenced. Provide a previously used workspace to avoid re-parsing common documents. - /// - /// Will be made public once finalized. - internal async Task ReadAsync(Stream input, OpenApiDiagnostic diagnostic, OpenApiWorkspace openApiWorkspace = null) - { - // Parse the OpenAPI Document - OpenApiDocument document = ParseStream(input, diagnostic); - - // Load Document into workspace and load all referenced documents - var workspace = openApiWorkspace ?? new OpenApiWorkspace(); - var settings = new OpenApiReaderSettings() - { - ExtensionParsers = _settings.ExtensionParsers, - RuleSet = _settings.RuleSet, - ReferenceResolution = ReferenceResolutionSetting.DoNotResolveReferences - }; - - var reader = new OpenApiStreamReader(settings); - var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkspace, new DefaultStreamLoader(), reader); - await workspaceLoader.LoadAsync(null,document); - - // Resolve References if requested - switch (_settings.ReferenceResolution) - { - case ReferenceResolutionSetting.ResolveAllReferences: - // Resolve references in documents - foreach (var item in workspace.Documents) - { - item.ResolveReferences(true); - } - break; - case ReferenceResolutionSetting.ResolveLocalReferences: - // Resolve references in documents - foreach (var item in workspace.Documents) - { - item.ResolveReferences(false); - } - break; - case - ReferenceResolutionSetting.DoNotResolveReferences: - break; - } - - // Validate loaded documents - foreach (var item in workspace.Documents) - { - ValidateDocument(diagnostic, item); - } - - return document; - } - - private OpenApiDocument ParseStream(Stream input, OpenApiDiagnostic diagnostic) - { - ParsingContext context; - YamlDocument yamlDocument; - - // Parse the YAML/JSON - try - { - yamlDocument = LoadYamlDocument(input); - } - catch (SyntaxErrorException ex) - { - diagnostic.Errors.Add(new OpenApiReaderError(ex)); - return new OpenApiDocument(); - } - - context = new ParsingContext - { - ExtensionParsers = _settings.ExtensionParsers - }; - - // Parse the OpenAPI Document - return context.Parse(yamlDocument, diagnostic); - } - - private void ValidateDocument(OpenApiDiagnostic diagnostic, OpenApiDocument document) - { - // Validate the document - if (_settings.RuleSet != null && _settings.RuleSet.Rules.Count > 0) - { - var errors = document.Validate(_settings.RuleSet); - foreach (var item in errors) - { - diagnostic.Errors.Add(item); - } + return new OpenApiTextReaderReader(_settings).Read(reader, out diagnostic); } } @@ -169,67 +47,10 @@ private void ValidateDocument(OpenApiDiagnostic diagnostic, OpenApiDocument docu /// Instance of newly created OpenApiDocument public T ReadFragment(Stream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiElement { - ParsingContext context; - YamlDocument yamlDocument; - diagnostic = new OpenApiDiagnostic(); - - // Parse the YAML/JSON - try - { - yamlDocument = LoadYamlDocument(input); - } - catch (SyntaxErrorException ex) - { - diagnostic.Errors.Add(new OpenApiReaderError(ex)); - return default(T); - } - - context = new ParsingContext - { - ExtensionParsers = _settings.ExtensionParsers - }; - - IOpenApiElement element = null; - - try - { - // Parse the OpenAPI element - element = context.ParseFragment(yamlDocument, version, diagnostic); - } - catch (OpenApiException ex) - { - diagnostic.Errors.Add(new OpenApiReaderError(ex)); - } - - // Validate the element - if (_settings.RuleSet != null && _settings.RuleSet.Rules.Count > 0) - { - var errors = element.Validate(_settings.RuleSet); - foreach (var item in errors) - { - diagnostic.Errors.Add(item); - } - } - - return (T)element; - } - - - /// - /// Helper method to turn streams into YamlDocument - /// - /// Stream containing YAML formatted text - /// Instance of a YamlDocument - internal static YamlDocument LoadYamlDocument(Stream input) - { - YamlDocument yamlDocument; - using (var streamReader = new StreamReader(input)) + using (var reader = new StreamReader(input)) { - var yamlStream = new YamlStream(); - yamlStream.Load(streamReader); - yamlDocument = yamlStream.Documents.First(); + return new OpenApiTextReaderReader(_settings).ReadFragment(reader, version, out diagnostic); } - return yamlDocument; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStringReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStringReader.cs index 82b3a3ce7..0cb9605dd 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStringReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStringReader.cs @@ -29,14 +29,9 @@ public OpenApiStringReader(OpenApiReaderSettings settings = null) /// public OpenApiDocument Read(string input, out OpenApiDiagnostic diagnostic) { - using (var memoryStream = new MemoryStream()) + using (var reader = new StringReader(input)) { - var writer = new StreamWriter(memoryStream); - writer.Write(input); - writer.Flush(); - memoryStream.Position = 0; - - return new OpenApiStreamReader(_settings).Read(memoryStream, out diagnostic); + return new OpenApiTextReaderReader(_settings).Read(reader, out diagnostic); } } @@ -45,15 +40,10 @@ public OpenApiDocument Read(string input, out OpenApiDiagnostic diagnostic) /// public T ReadFragment(string input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiElement { - using (var memoryStream = new MemoryStream()) + using (var reader = new StringReader(input)) { - var writer = new StreamWriter(memoryStream); - writer.Write(input); - writer.Flush(); - memoryStream.Position = 0; - - return new OpenApiStreamReader(_settings).ReadFragment(memoryStream, version, out diagnostic); + return new OpenApiTextReaderReader(_settings).ReadFragment(reader, version, out diagnostic); } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs new file mode 100644 index 000000000..c604007c7 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using System.Linq; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Interface; +using SharpYaml; +using SharpYaml.Serialization; + +namespace Microsoft.OpenApi.Readers +{ + /// + /// Service class for converting contents of TextReader into OpenApiDocument instances + /// + public class OpenApiTextReaderReader : IOpenApiReader + { + private readonly OpenApiReaderSettings _settings; + + /// + /// Create stream reader with custom settings if desired. + /// + /// + public OpenApiTextReaderReader(OpenApiReaderSettings settings = null) + { + _settings = settings ?? new OpenApiReaderSettings(); + } + + /// + /// Reads the stream input and parses it into an Open API document. + /// + /// TextReader containing OpenAPI description to parse. + /// Returns diagnostic object containing errors detected during parsing + /// Instance of newly created OpenApiDocument + public OpenApiDocument Read(TextReader input, out OpenApiDiagnostic diagnostic) + { + YamlDocument yamlDocument; + + // Parse the YAML/JSON + try + { + yamlDocument = LoadYamlDocument(input); + } + catch (YamlException ex) + { + diagnostic = new OpenApiDiagnostic(); + diagnostic.Errors.Add(new OpenApiError($"#char={ex.Start.Line}", ex.Message)); + return new OpenApiDocument(); + } + + return new OpenApiYamlDocumentReader(this._settings).Read(yamlDocument, out diagnostic); + } + /// + /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. + /// + /// TextReader containing OpenAPI description to parse. + /// Version of the OpenAPI specification that the fragment conforms to. + /// Returns diagnostic object containing errors detected during parsing + /// Instance of newly created OpenApiDocument + public T ReadFragment(TextReader input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiElement + { + YamlDocument yamlDocument; + + // Parse the YAML/JSON + try + { + yamlDocument = LoadYamlDocument(input); + } + catch (YamlException ex) + { + diagnostic = new OpenApiDiagnostic(); + diagnostic.Errors.Add(new OpenApiError($"#line={ex.Start.Line}", ex.Message)); + return default(T); + } + + return new OpenApiYamlDocumentReader(this._settings).ReadFragment(yamlDocument, version, out diagnostic); + } + + /// + /// Helper method to turn streams into YamlDocument + /// + /// Stream containing YAML formatted text + /// Instance of a YamlDocument + static YamlDocument LoadYamlDocument(TextReader input) + { + var yamlStream = new YamlStream(); + yamlStream.Load(input); + return yamlStream.Documents.First(); + } + } +} diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs new file mode 100644 index 000000000..215d7eefc --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Interface; +using Microsoft.OpenApi.Readers.Services; +using Microsoft.OpenApi.Services; +using SharpYaml.Serialization; + +namespace Microsoft.OpenApi.Readers +{ + /// + /// Service class for converting contents of TextReader into OpenApiDocument instances + /// + internal class OpenApiYamlDocumentReader : IOpenApiReader + { + private readonly OpenApiReaderSettings _settings; + + /// + /// Create stream reader with custom settings if desired. + /// + /// + public OpenApiYamlDocumentReader(OpenApiReaderSettings settings = null) + { + _settings = settings ?? new OpenApiReaderSettings(); + } + + /// + /// Reads the stream input and parses it into an Open API document. + /// + /// TextReader containing OpenAPI description to parse. + /// Returns diagnostic object containing errors detected during parsing + /// Instance of newly created OpenApiDocument + public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic) + { + diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic) + { + ExtensionParsers = _settings.ExtensionParsers, + BaseUrl = _settings.BaseUrl + }; + + OpenApiDocument document = null; + try + { + // Parse the OpenAPI Document + document = context.Parse(input); + + // Resolve References if requested + switch (_settings.ReferenceResolution) + { + case ReferenceResolutionSetting.ResolveAllReferences: + throw new ArgumentException(Properties.SRResource.CannotResolveRemoteReferencesSynchronously); + case ReferenceResolutionSetting.ResolveLocalReferences: + var errors = document.ResolveReferences(false); + + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } + break; + case ReferenceResolutionSetting.DoNotResolveReferences: + break; + } + } + catch (OpenApiException ex) + { + diagnostic.Errors.Add(new OpenApiError(ex)); + } + + // Validate the document + if (_settings.RuleSet != null && _settings.RuleSet.Rules.Count > 0) + { + var errors = document.Validate(_settings.RuleSet); + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } + } + + return document; + } + /// + /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. + /// + /// TextReader containing OpenAPI description to parse. + /// Version of the OpenAPI specification that the fragment conforms to. + /// Returns diagnostic object containing errors detected during parsing + /// Instance of newly created OpenApiDocument + public T ReadFragment(YamlDocument input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiElement + { + diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic) + { + ExtensionParsers = _settings.ExtensionParsers + }; + + IOpenApiElement element = null; + try + { + // Parse the OpenAPI element + element = context.ParseFragment(input, version); + } + catch (OpenApiException ex) + { + diagnostic.Errors.Add(new OpenApiError(ex)); + } + + // Validate the element + if (_settings.RuleSet != null && _settings.RuleSet.Rules.Count > 0) + { + var errors = element.Validate(_settings.RuleSet); + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } + } + + return (T)element; + } + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMap.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMap.cs new file mode 100644 index 000000000..a135f7f02 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMap.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Readers.ParseNodes +{ + internal class AnyFieldMap : Dictionary> + { + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMapParameter.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMapParameter.cs new file mode 100644 index 000000000..30aa0dbca --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyFieldMapParameter.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Readers.ParseNodes +{ + internal class AnyFieldMapParameter + { + /// + /// Constructor. + /// + public AnyFieldMapParameter( + Func propertyGetter, + Action propertySetter, + Func schemaGetter) + { + this.PropertyGetter = propertyGetter; + this.PropertySetter = propertySetter; + this.SchemaGetter = schemaGetter; + } + + /// + /// Function to retrieve the value of the property. + /// + public Func PropertyGetter { get; } + + /// + /// Function to set the value of the property. + /// + public Action PropertySetter { get; } + + /// + /// Function to get the schema to apply to the property. + /// + public Func SchemaGetter { get; } + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMap.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMap.cs new file mode 100644 index 000000000..dbc9eabeb --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMap.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Readers.ParseNodes +{ + internal class AnyListFieldMap : Dictionary> + { + + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMapParameter.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMapParameter.cs new file mode 100644 index 000000000..cfa1c3702 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyListFieldMapParameter.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Readers.ParseNodes +{ + internal class AnyListFieldMapParameter + { + /// + /// Constructor + /// + public AnyListFieldMapParameter( + Func> propertyGetter, + Action> propertySetter, + Func schemaGetter) + { + this.PropertyGetter = propertyGetter; + this.PropertySetter = propertySetter; + this.SchemaGetter = schemaGetter; + } + + /// + /// Function to retrieve the value of the property. + /// + public Func> PropertyGetter { get; } + + /// + /// Function to set the value of the property. + /// + public Action> PropertySetter { get; } + + /// + /// Function to get the schema to apply to the property. + /// + public Func SchemaGetter { get; } + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMap.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMap.cs new file mode 100644 index 000000000..abd4f4466 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMap.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Readers.ParseNodes +{ + internal class AnyMapFieldMap : Dictionary> + { + + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMapParameter.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMapParameter.cs new file mode 100644 index 000000000..1aa899978 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/AnyMapFieldMapParameter.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Readers.ParseNodes +{ + internal class AnyMapFieldMapParameter + { + /// + /// Constructor + /// + public AnyMapFieldMapParameter( + Func> propertyMapGetter, + Func propertyGetter, + Action propertySetter, + Func schemaGetter) + { + this.PropertyMapGetter = propertyMapGetter; + this.PropertyGetter = propertyGetter; + this.PropertySetter = propertySetter; + this.SchemaGetter = schemaGetter; + } + + /// + /// Function to retrieve the property that is a map from string to an inner element containing IOpenApiAny. + /// + public Func> PropertyMapGetter { get; } + + /// + /// Function to retrieve the value of the property from an inner element. + /// + public Func PropertyGetter { get; } + + /// + /// Function to set the value of the property. + /// + public Action PropertySetter { get; } + + /// + /// Function to get the schema to apply to the property. + /// + public Func SchemaGetter { get; } + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/FixedFieldMap.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/FixedFieldMap.cs index 26c1cfda2..50cc5e4b7 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/FixedFieldMap.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/FixedFieldMap.cs @@ -9,4 +9,4 @@ namespace Microsoft.OpenApi.Readers.ParseNodes internal class FixedFieldMap : Dictionary> { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/JsonPointerExtensions.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/JsonPointerExtensions.cs index de4d14aa3..d30863955 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/JsonPointerExtensions.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/JsonPointerExtensions.cs @@ -53,4 +53,4 @@ public static YamlNode Find(this JsonPointer currentPointer, YamlNode baseYamlNo } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs index f7630c617..e1149cc5a 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs @@ -17,9 +17,8 @@ internal class ListNode : ParseNode, IEnumerable { private readonly YamlSequenceNode _nodeList; - public ListNode(ParsingContext context, OpenApiDiagnostic diagnostic, YamlSequenceNode sequenceNode) : base( - context, - diagnostic) + public ListNode(ParsingContext context, YamlSequenceNode sequenceNode) : base( + context) { _nodeList = sequenceNode; } @@ -32,14 +31,14 @@ public override List CreateList(Func map) $"Expected list at line {_nodeList.Start.Line} while parsing {typeof(T).Name}"); } - return _nodeList.Select(n => map(new MapNode(Context, Diagnostic, n as YamlMappingNode))) + return _nodeList.Select(n => map(new MapNode(Context, n as YamlMappingNode))) .Where(i => i != null) .ToList(); } public override List CreateListOfAny() { - return _nodeList.Select(n => ParseNode.Create(Context, Diagnostic,n).CreateAny()) + return _nodeList.Select(n => ParseNode.Create(Context, n).CreateAny()) .Where(i => i != null) .ToList(); } @@ -52,12 +51,12 @@ public override List CreateSimpleList(Func map) $"Expected list at line {_nodeList.Start.Line} while parsing {typeof(T).Name}"); } - return _nodeList.Select(n => map(new ValueNode(Context, Diagnostic, (YamlScalarNode)n))).ToList(); + return _nodeList.Select(n => map(new ValueNode(Context, n))).ToList(); } public IEnumerator GetEnumerator() { - return _nodeList.Select(n => Create(Context, Diagnostic, n)).ToList().GetEnumerator(); + return _nodeList.Select(n => Create(Context, n)).ToList().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() @@ -80,4 +79,4 @@ public override IOpenApiAny CreateAny() return array; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs index ef18cf11a..26fc81076 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs @@ -9,6 +9,7 @@ using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Exceptions; using SharpYaml.Schemas; using SharpYaml.Serialization; @@ -22,24 +23,23 @@ internal class MapNode : ParseNode, IEnumerable private readonly YamlMappingNode _node; private readonly List _nodes; - public MapNode(ParsingContext context, OpenApiDiagnostic diagnostic, string yamlString) : - this(context, diagnostic, (YamlMappingNode)YamlHelper.ParseYamlString(yamlString)) + public MapNode(ParsingContext context, string yamlString) : + this(context, (YamlMappingNode)YamlHelper.ParseYamlString(yamlString)) { } - public MapNode(ParsingContext context, OpenApiDiagnostic diagnostic, YamlMappingNode node) : base( - context, - diagnostic) + public MapNode(ParsingContext context, YamlNode node) : base( + context) { - if (node == null) + if (!(node is YamlMappingNode mapNode)) { - throw new OpenApiException("Expected map"); + throw new OpenApiReaderException("Expected map.", node); } - this._node = node; + this._node = mapNode; _nodes = this._node.Children - .Select(kvp => new PropertyNode(Context, Diagnostic, kvp.Key.GetScalarValue(), kvp.Value)) + .Select(kvp => new PropertyNode(Context, kvp.Key.GetScalarValue(), kvp.Value)) .Cast() .ToList(); } @@ -51,7 +51,7 @@ public PropertyNode this[string key] YamlNode node = null; if (this._node.Children.TryGetValue(new YamlScalarNode(key), out node)) { - return new PropertyNode(Context, Diagnostic, key, this._node.Children[new YamlScalarNode(key)]); + return new PropertyNode(Context, key, this._node.Children[new YamlScalarNode(key)]); } return null; @@ -72,43 +72,48 @@ public override Dictionary CreateMap(Func map) key = n.Key.GetScalarValue(), value = n.Value as YamlMappingNode == null ? default(T) - : map(new MapNode(Context, Diagnostic, n.Value as YamlMappingNode)) + : map(new MapNode(Context, n.Value as YamlMappingNode)) }); return nodes.ToDictionary(k => k.key, v => v.value); } - public override Dictionary CreateMapWithReference( - ReferenceType referenceType, - Func map) - { - var yamlMap = _node; - if (yamlMap == null) - { - throw new OpenApiException($"Expected map at line {yamlMap.Start.Line} while parsing {typeof(T).Name}"); - } - - var nodes = yamlMap.Select( - n => { - var entry = new - { - key = n.Key.GetScalarValue(), - value = map(new MapNode(Context, Diagnostic, (YamlMappingNode)n.Value)) - }; - if (entry.value == null) - { - return null; // Body Parameters shouldn't be converted to Parameters - } - entry.value.Reference = new OpenApiReference() - { - Type = referenceType, - Id = entry.key - }; - return entry; - } - ); - return nodes.Where(n => n!= null).ToDictionary(k => k.key, v => v.value); - } + public override Dictionary CreateMapWithReference( + ReferenceType referenceType, + Func map) + { + var yamlMap = _node; + if (yamlMap == null) + { + throw new OpenApiException($"Expected map at line {yamlMap.Start.Line} while parsing {typeof(T).Name}"); + } + + var nodes = yamlMap.Select( + n => + { + var entry = new + { + key = n.Key.GetScalarValue(), + value = map(new MapNode(Context, (YamlMappingNode)n.Value)) + }; + if (entry.value == null) + { + return null; // Body Parameters shouldn't be converted to Parameters + } + // If the component isn't a reference to another component, then point it to itself. + if (entry.value.Reference == null) + { + entry.value.Reference = new OpenApiReference() + { + Type = referenceType, + Id = entry.key + }; + } + return entry; + } + ); + return nodes.Where(n => n != null).ToDictionary(k => k.key, v => v.value); + } public override Dictionary CreateSimpleMap(Func map) { @@ -122,7 +127,7 @@ public override Dictionary CreateSimpleMap(Func map) n => new { key = n.Key.GetScalarValue(), - value = map(new ValueNode(Context, Diagnostic, (YamlScalarNode)n.Value)) + value = map(new ValueNode(Context, (YamlScalarNode)n.Value)) }); return nodes.ToDictionary(k => k.key, v => v.value); } @@ -139,7 +144,7 @@ IEnumerator IEnumerable.GetEnumerator() public override string GetRaw() { - var x = new Serializer(new SerializerSettings(new JsonSchema()) {EmitJsonComptible = true}); + var x = new Serializer(new SerializerSettings(new JsonSchema()) { EmitJsonComptible = true }); return x.Serialize(_node); } @@ -147,10 +152,10 @@ public T GetReferencedObject(ReferenceType referenceType, string referenceId) where T : IOpenApiReferenceable, new() { return new T() - { - UnresolvedReference = true, - Reference = Context.VersionService.ConvertToOpenApiReference(referenceId,referenceType) - }; + { + UnresolvedReference = true, + Reference = Context.VersionService.ConvertToOpenApiReference(referenceId, referenceType) + }; } public string GetReferencePointer() @@ -191,4 +196,4 @@ public override IOpenApiAny CreateAny() return apiObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs new file mode 100644 index 000000000..ae9254fe8 --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/OpenApiAnyConverter.cs @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Readers.ParseNodes +{ + internal static class OpenApiAnyConverter + { + /// + /// Converts the s in the given + /// into the appropriate type based on the given . + /// For those strings that the schema does not specify the type for, convert them into + /// the most specific type based on the value. + /// + public static IOpenApiAny GetSpecificOpenApiAny(IOpenApiAny openApiAny, OpenApiSchema schema = null) + { + if (openApiAny is OpenApiArray openApiArray) + { + var newArray = new OpenApiArray(); + foreach (var element in openApiArray) + { + newArray.Add(GetSpecificOpenApiAny(element, schema?.Items)); + } + + return newArray; + } + + if (openApiAny is OpenApiObject openApiObject) + { + var newObject = new OpenApiObject(); + + foreach (var key in openApiObject.Keys.ToList()) + { + if (schema?.Properties != null && schema.Properties.TryGetValue(key, out var property)) + { + newObject[key] = GetSpecificOpenApiAny(openApiObject[key], property); + } + else + { + newObject[key] = GetSpecificOpenApiAny(openApiObject[key], schema?.AdditionalProperties); + } + } + + return newObject; + } + + if (!(openApiAny is OpenApiString)) + { + return openApiAny; + } + + var value = ((OpenApiString)openApiAny).Value; + var type = schema?.Type; + var format = schema?.Format; + + if (((OpenApiString)openApiAny).IsExplicit()) + { + // More narrow type detection for explicit strings, only check types that are passed as strings + if (schema == null) + { + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) + { + return new OpenApiDateTime(dateTimeValue); + } + } + else if (type == "string") + { + if (format == "byte") + { + try + { + return new OpenApiByte(Convert.FromBase64String(value)); + } + catch (FormatException) + { } + } + + if (format == "binary") + { + try + { + return new OpenApiBinary(Encoding.UTF8.GetBytes(value)); + } + catch (EncoderFallbackException) + { } + } + + if (format == "date") + { + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateValue)) + { + return new OpenApiDate(dateValue.Date); + } + } + + if (format == "date-time") + { + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) + { + return new OpenApiDateTime(dateTimeValue); + } + } + + if (format == "password") + { + return new OpenApiPassword(value); + } + } + + return openApiAny; + } + + if (value == null || value == "null") + { + return new OpenApiNull(); + } + + if (schema?.Type == null) + { + if (value == "true") + { + return new OpenApiBoolean(true); + } + + if (value == "false") + { + return new OpenApiBoolean(false); + } + + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + return new OpenApiInteger(intValue); + } + + if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) + { + return new OpenApiLong(longValue); + } + + if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + { + return new OpenApiDouble(doubleValue); + } + + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) + { + return new OpenApiDateTime(dateTimeValue); + } + } + else + { + if (type == "integer" && format == "int32") + { + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + return new OpenApiInteger(intValue); + } + } + + if (type == "integer" && format == "int64") + { + if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) + { + return new OpenApiLong(longValue); + } + } + + if (type == "integer") + { + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + { + return new OpenApiInteger(intValue); + } + } + + if (type == "number" && format == "float") + { + if (float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var floatValue)) + { + return new OpenApiFloat(floatValue); + } + } + + if (type == "number" && format == "double") + { + if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + { + return new OpenApiDouble(doubleValue); + } + } + + if (type == "number") + { + if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue)) + { + return new OpenApiDouble(doubleValue); + } + } + + if (type == "string" && format == "byte") + { + try + { + return new OpenApiByte(Convert.FromBase64String(value)); + } + catch (FormatException) + { } + } + + // binary + if (type == "string" && format == "binary") + { + try + { + return new OpenApiBinary(Encoding.UTF8.GetBytes(value)); + } + catch (EncoderFallbackException) + { } + } + + if (type == "string" && format == "date") + { + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateValue)) + { + return new OpenApiDate(dateValue.Date); + } + } + + if (type == "string" && format == "date-time") + { + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTimeValue)) + { + return new OpenApiDateTime(dateTimeValue); + } + } + + if (type == "string" && format == "password") + { + return new OpenApiPassword(value); + } + + if (type == "string") + { + return openApiAny; + } + + if (type == "boolean") + { + if (bool.TryParse(value, out var booleanValue)) + { + return new OpenApiBoolean(booleanValue); + } + } + } + + // If data conflicts with the given type, return a string. + // This converter is used in the parser, so it does not perform any validations, + // but the validator can be used to validate whether the data and given type conflicts. + return openApiAny; + } + } +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs index a80176317..25f0eabc1 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs @@ -8,59 +8,54 @@ using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Exceptions; using SharpYaml.Serialization; namespace Microsoft.OpenApi.Readers.ParseNodes { internal abstract class ParseNode { - protected ParseNode(ParsingContext parsingContext, OpenApiDiagnostic diagnostic) + protected ParseNode(ParsingContext parsingContext) { Context = parsingContext; - Diagnostic = diagnostic; } public ParsingContext Context { get; } - public OpenApiDiagnostic Diagnostic { get; } - public MapNode CheckMapNode(string nodeName) { - var mapNode = this as MapNode; - if (mapNode == null) + if (!(this is MapNode mapNode)) { - throw new OpenApiException($"{nodeName} must be a map/object"); + throw new OpenApiReaderException($"{nodeName} must be a map/object"); } return mapNode; } - public static ParseNode Create(ParsingContext context, OpenApiDiagnostic diagnostic, YamlNode node) + public static ParseNode Create(ParsingContext context, YamlNode node) { - var listNode = node as YamlSequenceNode; - if (listNode != null) + if (node is YamlSequenceNode listNode) { - return new ListNode(context, diagnostic, listNode); + return new ListNode(context, listNode); } - var mapNode = node as YamlMappingNode; - if (mapNode != null) + if (node is YamlMappingNode mapNode) { - return new MapNode(context, diagnostic, mapNode); + return new MapNode(context, mapNode); } - return new ValueNode(context, diagnostic, node as YamlScalarNode); + return new ValueNode(context, node as YamlScalarNode); } public virtual List CreateList(Func map) { - throw new OpenApiException("Cannot create list from this type of node."); + throw new OpenApiReaderException("Cannot create list from this type of node."); } public virtual Dictionary CreateMap(Func map) { - throw new OpenApiException("Cannot create map from this type of node."); + throw new OpenApiReaderException("Cannot create map from this type of node."); } public virtual Dictionary CreateMapWithReference( @@ -68,38 +63,38 @@ public virtual Dictionary CreateMapWithReference( Func map) where T : class, IOpenApiReferenceable { - throw new OpenApiException("Cannot create map from this reference."); + throw new OpenApiReaderException("Cannot create map from this reference."); } public virtual List CreateSimpleList(Func map) { - throw new OpenApiException("Cannot create simple list from this type of node."); + throw new OpenApiReaderException("Cannot create simple list from this type of node."); } public virtual Dictionary CreateSimpleMap(Func map) { - throw new OpenApiException("Cannot create simple map from this type of node."); + throw new OpenApiReaderException("Cannot create simple map from this type of node."); } public virtual IOpenApiAny CreateAny() { - throw new OpenApiException("Cannot create an Any object this type of node."); + throw new OpenApiReaderException("Cannot create an Any object this type of node."); } public virtual string GetRaw() { - throw new OpenApiException("Cannot get raw value from this type of node."); + throw new OpenApiReaderException("Cannot get raw value from this type of node."); } public virtual string GetScalarValue() { - throw new OpenApiException("Cannot create a scalar value from this type of node."); + throw new OpenApiReaderException("Cannot create a scalar value from this type of node."); } public virtual List CreateListOfAny() { - throw new OpenApiException("Cannot create a list from this type of node."); + throw new OpenApiReaderException("Cannot create a list from this type of node."); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/PatternFieldMap.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/PatternFieldMap.cs index 8dadfcee2..bbd153688 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/PatternFieldMap.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/PatternFieldMap.cs @@ -9,4 +9,4 @@ namespace Microsoft.OpenApi.Readers.ParseNodes internal class PatternFieldMap : Dictionary, Action> { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs index 0714d829a..2dd2c7e8a 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs @@ -7,18 +7,18 @@ using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Exceptions; using SharpYaml.Serialization; namespace Microsoft.OpenApi.Readers.ParseNodes { internal class PropertyNode : ParseNode { - public PropertyNode(ParsingContext context, OpenApiDiagnostic diagnostic, string name, YamlNode node) : base( - context, - diagnostic) + public PropertyNode(ParsingContext context, string name, YamlNode node) : base( + context) { Name = name; - Value = Create(context, diagnostic, node); + Value = Create(context, node); } public string Name { get; set; } @@ -40,10 +40,14 @@ public void ParseField( Context.StartObject(Name); fixedFieldMap(parentInstance, Value); } + catch (OpenApiReaderException ex) + { + Context.Diagnostic.Errors.Add(new OpenApiError(ex)); + } catch (OpenApiException ex) { ex.Pointer = Context.GetLocation(); - Diagnostic.Errors.Add(new OpenApiReaderError(ex)); + Context.Diagnostic.Errors.Add(new OpenApiError(ex)); } finally { @@ -60,10 +64,14 @@ public void ParseField( Context.StartObject(Name); map(parentInstance, Name, Value); } + catch (OpenApiReaderException ex) + { + Context.Diagnostic.Errors.Add(new OpenApiError(ex)); + } catch (OpenApiException ex) { ex.Pointer = Context.GetLocation(); - Diagnostic.Errors.Add(new OpenApiReaderError(ex)); + Context.Diagnostic.Errors.Add(new OpenApiError(ex)); } finally { @@ -72,8 +80,8 @@ public void ParseField( } else { - Diagnostic.Errors.Add( - new OpenApiReaderError(Context, $"{Name} is not a valid property")); + Context.Diagnostic.Errors.Add( + new OpenApiError("", $"{Name} is not a valid property at {Context.GetLocation()}")); } } } @@ -83,4 +91,4 @@ public override IOpenApiAny CreateAny() throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/RootNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/RootNode.cs index 014e2e71f..42909bee6 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/RootNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/RootNode.cs @@ -14,8 +14,7 @@ internal class RootNode : ParseNode public RootNode( ParsingContext context, - OpenApiDiagnostic diagnostic, - YamlDocument yamlDocument) : base(context, diagnostic) + YamlDocument yamlDocument) : base(context) { _yamlDocument = yamlDocument; } @@ -28,12 +27,12 @@ public ParseNode Find(JsonPointer referencePointer) return null; } - return Create(Context, Diagnostic, yamlNode); + return Create(Context, yamlNode); } public MapNode GetMap() { - return new MapNode(Context, Diagnostic, (YamlMappingNode)_yamlDocument.RootNode); + return new MapNode(Context, (YamlMappingNode)_yamlDocument.RootNode); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs index 44d23d856..68f4bd7ea 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Readers.Exceptions; +using SharpYaml; using SharpYaml.Serialization; namespace Microsoft.OpenApi.Readers.ParseNodes @@ -12,24 +12,20 @@ internal class ValueNode : ParseNode { private readonly YamlScalarNode _node; - public ValueNode(ParsingContext context, OpenApiDiagnostic diagnostic, YamlScalarNode scalarNode) : base( - context, - diagnostic) + public ValueNode(ParsingContext context, YamlNode node) : base( + context) { + if (!(node is YamlScalarNode scalarNode)) + { + throw new OpenApiReaderException("Expected a value.", node); + } _node = scalarNode; } public override string GetScalarValue() { - var scalarNode = _node; - - if (scalarNode == null) - { - throw new OpenApiException($"Expected scalar at line {_node.Start.Line}"); - } - - return scalarNode.Value; - } + return _node.Value; + } /// /// Create a @@ -38,48 +34,7 @@ public override string GetScalarValue() public override IOpenApiAny CreateAny() { var value = GetScalarValue(); - - if (value == null || value == "null") - { - return new OpenApiNull(); - } - - if (value == "true") - { - return new OpenApiBoolean(true); - } - - if (value == "false") - { - return new OpenApiBoolean(false); - } - - if (int.TryParse(value, out var intValue)) - { - return new OpenApiInteger(intValue); - } - - if (long.TryParse(value, out var longValue)) - { - return - new OpenApiLong( - longValue); - } - - if (double.TryParse(value, out var dblValue)) - { - return - new OpenApiDouble( - dblValue); // Note(darrmi): This may be better as decimal. Further investigation required. - } - - if (DateTimeOffset.TryParse(value, out var datetimeValue)) - { - return new OpenApiDateTime(datetimeValue); - } - - // if we can't identify the type of value, return it as string. - return new OpenApiString(value); + return new OpenApiString(value, this._node.Style == ScalarStyle.SingleQuoted || this._node.Style == ScalarStyle.DoubleQuoted || this._node.Style == ScalarStyle.Literal || this._node.Style == ScalarStyle.Folded); } } } diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs index b994301d3..9c7277136 100644 --- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs +++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs @@ -23,21 +23,35 @@ public class ParsingContext { private readonly Stack _currentLocation = new Stack(); private readonly Dictionary _tempStorage = new Dictionary(); - private IOpenApiVersionService _versionService; - private readonly Dictionary> _loopStacks = new Dictionary>(); - internal Dictionary> ExtensionParsers { get; set; } = new Dictionary>(); + private readonly Dictionary> _scopedTempStorage = new Dictionary>(); + private readonly Dictionary> _loopStacks = new Dictionary>(); + internal Dictionary> ExtensionParsers { get; set; } = new Dictionary>(); internal RootNode RootNode { get; set; } internal List Tags { get; private set; } = new List(); + internal Uri BaseUrl { get; set; } + + /// + /// Diagnostic object that returns metadata about the parsing process. + /// + public OpenApiDiagnostic Diagnostic { get; } + + /// + /// Create Parsing Context + /// + /// Provide instance for diagnotic object for collecting and accessing information about the parsing. + public ParsingContext(OpenApiDiagnostic diagnostic) + { + Diagnostic = diagnostic; + } /// /// Initiates the parsing process. Not thread safe and should only be called once on a parsing context /// /// Yaml document to parse. - /// Diagnostic object which will return diagnostic results of the operation. /// An OpenApiDocument populated based on the passed yamlDocument - internal OpenApiDocument Parse(YamlDocument yamlDocument, OpenApiDiagnostic diagnostic) + internal OpenApiDocument Parse(YamlDocument yamlDocument) { - RootNode = new RootNode(this, diagnostic, yamlDocument); + RootNode = new RootNode(this, yamlDocument); var inputVersion = GetVersion(RootNode); @@ -48,13 +62,13 @@ internal OpenApiDocument Parse(YamlDocument yamlDocument, OpenApiDiagnostic diag case string version when version == "2.0": VersionService = new OpenApiV2VersionService(); doc = VersionService.LoadDocument(RootNode); - diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0; + this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0; break; case string version when version.StartsWith("3.0"): VersionService = new OpenApiV3VersionService(); doc = VersionService.LoadDocument(RootNode); - diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_0; + this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_0; break; default: @@ -69,11 +83,10 @@ internal OpenApiDocument Parse(YamlDocument yamlDocument, OpenApiDiagnostic diag /// /// /// OpenAPI version of the fragment - /// Diagnostic object which will return diagnostic results of the operation. /// An OpenApiDocument populated based on the passed yamlDocument - internal T ParseFragment(YamlDocument yamlDocument, OpenApiSpecVersion version, OpenApiDiagnostic diagnostic) where T: IOpenApiElement + internal T ParseFragment(YamlDocument yamlDocument, OpenApiSpecVersion version) where T : IOpenApiElement { - var node = ParseNode.Create(this, diagnostic, yamlDocument.RootNode); + var node = ParseNode.Create(this, yamlDocument.RootNode); T element = default(T); @@ -113,17 +126,7 @@ private static string GetVersion(RootNode rootNode) /// /// Service providing all Version specific conversion functions /// - internal IOpenApiVersionService VersionService - { - get - { - return _versionService; - } - set - { - _versionService = value; - } - } + internal IOpenApiVersionService VersionService { get; set; } /// /// End the current object. @@ -144,22 +147,46 @@ public string GetLocation() /// /// Gets the value from the temporary storage matching the given key. /// - public T GetFromTempStorage(string key) where T : class + public T GetFromTempStorage(string key, object scope = null) { - if (_tempStorage.TryGetValue(key, out var value)) + Dictionary storage; + + if (scope == null) + { + storage = _tempStorage; + } + else if (!_scopedTempStorage.TryGetValue(scope, out storage)) { - return (T)value; + return default(T); } - return null; + return storage.TryGetValue(key, out var value) ? (T)value : default(T); } /// /// Sets the temporary storge for this key and value. /// - public void SetTempStorage(string key, object value) + public void SetTempStorage(string key, object value, object scope = null) { - _tempStorage[key] = value; + Dictionary storage; + + if (scope == null) + { + storage = _tempStorage; + } + else if (!_scopedTempStorage.TryGetValue(scope, out storage)) + { + storage = _scopedTempStorage[scope] = new Dictionary(); + } + + if (value == null) + { + storage.Remove(key); + } + else + { + storage[key] = value; + } } /// @@ -189,7 +216,8 @@ public bool PushLoop(string loopId, string key) { stack.Push(key); return true; - } else + } + else { return false; // Loop detected } @@ -217,4 +245,4 @@ public void PopLoop(string loopid) } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/Properties/AssemblyInfo.cs b/src/Microsoft.OpenApi.Readers/Properties/AssemblyInfo.cs index 1d8941520..b42e91bd1 100644 --- a/src/Microsoft.OpenApi.Readers/Properties/AssemblyInfo.cs +++ b/src/Microsoft.OpenApi.Readers/Properties/AssemblyInfo.cs @@ -5,4 +5,4 @@ [assembly: InternalsVisibleTo( - "Microsoft.OpenApi.Readers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] \ No newline at end of file + "Microsoft.OpenApi.Readers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiContactDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiContactDeserializer.cs index 3bcddffc0..99bc4451a 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiContactDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiContactDeserializer.cs @@ -38,7 +38,7 @@ internal static partial class OpenApiV2Deserializer private static PatternFieldMap _contactPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiContact LoadContact(ParseNode node) @@ -51,4 +51,4 @@ public static OpenApiContact LoadContact(ParseNode node) return contact; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs index 559f325ce..6302eaf84 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiDocumentDeserializer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; @@ -69,13 +70,14 @@ internal static partial class OpenApiV2Deserializer ReferenceType.Parameter, LoadParameter); - o.Components.RequestBodies = n.CreateMapWithReference(ReferenceType.RequestBody, p => + o.Components.RequestBodies = n.CreateMapWithReference(ReferenceType.RequestBody, p => { - var parameter = LoadParameter(p, evenBody: true); - if (parameter.In == null) + var parameter = LoadParameter(p, loadRequestBody: true); + if (parameter != null) { - return CreateRequestBody(n.Context,parameter); + return CreateRequestBody(n.Context, parameter); } + return null; } ); @@ -116,24 +118,104 @@ internal static partial class OpenApiV2Deserializer private static PatternFieldMap _openApiPatternFields = new PatternFieldMap { // We have no semantics to verify X- nodes, therefore treat them as just values. - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; - private static void MakeServers(IList servers, ParsingContext context) + private static void MakeServers(IList servers, ParsingContext context, RootNode rootNode) { var host = context.GetFromTempStorage("host"); var basePath = context.GetFromTempStorage("basePath"); var schemes = context.GetFromTempStorage>("schemes"); + Uri defaultUrl = rootNode.Context.BaseUrl; + + // If nothing is provided, don't create a server + if (host == null && basePath == null && schemes == null) + { + return; + } - if (schemes != null) + //Validate host + if (host != null && !IsHostValid(host)) + { + rootNode.Context.Diagnostic.Errors.Add(new OpenApiError(rootNode.Context.GetLocation(), "Invalid host")); + return; + } + + // Fill in missing information based on the defaultUrl + if (defaultUrl != null) + { + host = host ?? defaultUrl.GetComponents(UriComponents.NormalizedHost, UriFormat.SafeUnescaped); + basePath = basePath ?? defaultUrl.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped); + schemes = schemes ?? new List { defaultUrl.GetComponents(UriComponents.Scheme, UriFormat.SafeUnescaped) }; + } + else if (String.IsNullOrEmpty(host) && String.IsNullOrEmpty(basePath)) + { + return; // Can't make a server object out of just a Scheme + } + + // Create the Server objects + if (schemes != null && schemes.Count > 0) { foreach (var scheme in schemes) { - var server = new OpenApiServer(); - server.Url = scheme + "://" + (host ?? "example.org/") + (basePath ?? "/"); + var server = new OpenApiServer + { + Url = BuildUrl(scheme, host, basePath) + }; + servers.Add(server); } } + else + { + var server = new OpenApiServer + { + Url = BuildUrl(null, host, basePath) + }; + + servers.Add(server); + } + + foreach (var server in servers) + { + // Server Urls are always appended to Paths and Paths must start with / + // so removing the slash prevents a double slash. + if (server.Url.EndsWith("/")) + { + server.Url = server.Url.Substring(0, server.Url.Length - 1); + } + } + } + + private static string BuildUrl(string scheme, string host, string basePath) + { + if (String.IsNullOrEmpty(scheme) && !String.IsNullOrEmpty(host)) + { + host = "//" + host; // The double slash prefix creates a relative url where the scheme is defined by the BaseUrl + } + + int? port = null; + + if (!String.IsNullOrEmpty(host) && host.Contains(":")) + { + var pieces = host.Split(':'); + host = pieces.First(); + port = int.Parse(pieces.Last(), CultureInfo.InvariantCulture); + } + + var uriBuilder = new UriBuilder() + { + Scheme = scheme, + Host = host, + Path = basePath + }; + + if (port != null) + { + uriBuilder.Port = port.Value; + } + + return uriBuilder.ToString(); } public static OpenApiDocument LoadOpenApi(RootNode rootNode) @@ -144,6 +226,17 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode) ParseMap(openApiNode, openApidoc, _openApiFixedFields, _openApiPatternFields); + if (openApidoc.Paths != null) + { + ProcessResponsesMediaTypes( + rootNode.GetMap(), + openApidoc.Paths.Values + .SelectMany(path => path.Operations?.Values ?? Enumerable.Empty()) + .SelectMany(operation => operation.Responses?.Values ?? Enumerable.Empty()), + openApiNode.Context); + } + + ProcessResponsesMediaTypes(rootNode.GetMap(), openApidoc.Components?.Responses?.Values, openApiNode.Context); // Post Process OpenApi Object if (openApidoc.Servers == null) @@ -151,12 +244,31 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode) openApidoc.Servers = new List(); } - MakeServers(openApidoc.Servers, openApiNode.Context); + MakeServers(openApidoc.Servers, openApiNode.Context, rootNode); FixRequestBodyReferences(openApidoc); return openApidoc; } + private static void ProcessResponsesMediaTypes(MapNode mapNode, IEnumerable responses, ParsingContext context) + { + if (responses != null) + { + foreach (var response in responses) + { + ProcessProduces(mapNode, response, context); + + if (response.Content != null) + { + foreach (var mediaType in response.Content.Values) + { + ProcessAnyFields(mapNode, mediaType, _mediaTypeAnyFields); + } + } + } + } + } + private static void FixRequestBodyReferences(OpenApiDocument doc) { // Walk all unresolved parameter references @@ -167,7 +279,19 @@ private static void FixRequestBodyReferences(OpenApiDocument doc) var walker = new OpenApiWalker(fixer); walker.Walk(doc); } + } + private static bool IsHostValid(string host) + { + //Check if the host contains :// + if (host.Contains(Uri.SchemeDelimiter)) + { + return false; + } + + //Check if the host (excluding port number) is a valid dns/ip address. + var hostPart = host.Split(':').First(); + return Uri.CheckHostName(hostPart) != UriHostNameType.Unknown; } } @@ -197,5 +321,7 @@ public override void Visit(OpenApiOperation operation) }; } } + + } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiExternalDocsDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiExternalDocsDeserializer.cs index b0d2df871..4d57e7b3f 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiExternalDocsDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiExternalDocsDeserializer.cs @@ -44,4 +44,4 @@ public static OpenApiExternalDocs LoadExternalDocs(ParseNode node) return externalDocs; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs index 3d8f2e2f6..32caf86aa 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiHeaderDeserializer.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using System; -using Microsoft.OpenApi.Any; +using System.Globalization; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.Exceptions; @@ -54,10 +54,10 @@ internal static partial class OpenApiV2Deserializer GetOrCreateSchema(o).Default = n.CreateAny(); } }, - { + { "maximum", (o, n) => { - GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -69,7 +69,7 @@ internal static partial class OpenApiV2Deserializer { "minimum", (o, n) => { - GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -81,13 +81,13 @@ internal static partial class OpenApiV2Deserializer { "maxLength", (o, n) => { - GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -99,13 +99,13 @@ internal static partial class OpenApiV2Deserializer { "maxItems", (o, n) => { - GetOrCreateSchema(o).MaxItems = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minItems", (o, n) => { - GetOrCreateSchema(o).MinItems = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -117,7 +117,7 @@ internal static partial class OpenApiV2Deserializer { "multipleOf", (o, n) => { - GetOrCreateSchema(o).MultipleOf = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MultipleOf = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -130,9 +130,33 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _headerPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; + private static readonly AnyFieldMap _headerAnyFields = + new AnyFieldMap + { + { + OpenApiConstants.Default, + new AnyFieldMapParameter( + p => p.Schema.Default, + (p, v) => p.Schema.Default = v, + p => p.Schema) + } + }; + + private static readonly AnyListFieldMap _headerAnyListFields = + new AnyListFieldMap + { + { + OpenApiConstants.Enum, + new AnyListFieldMapParameter( + p => p.Schema.Enum, + (p, v) => p.Schema.Enum = v, + p => p.Schema) + }, + }; + public static OpenApiHeader LoadHeader(ParseNode node) { var mapNode = node.CheckMapNode("header"); @@ -149,6 +173,9 @@ public static OpenApiHeader LoadHeader(ParseNode node) node.Context.SetTempStorage("schema", null); } + ProcessAnyFields(mapNode, header, _headerAnyFields); + ProcessAnyListFields(mapNode, header, _headerAnyListFields); + return header; } @@ -172,4 +199,4 @@ private static void LoadStyle(OpenApiHeader header, string style) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiInfoDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiInfoDeserializer.cs index c546254cb..5854672d3 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiInfoDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiInfoDeserializer.cs @@ -57,7 +57,7 @@ internal static partial class OpenApiV2Deserializer private static PatternFieldMap _infoPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiInfo LoadInfo(ParseNode node) @@ -71,4 +71,4 @@ public static OpenApiInfo LoadInfo(ParseNode node) return info; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiLicenseDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiLicenseDeserializer.cs index 963c572a5..4c4009f57 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiLicenseDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiLicenseDeserializer.cs @@ -32,7 +32,7 @@ internal static partial class OpenApiV2Deserializer private static PatternFieldMap _licensePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiLicense LoadLicense(ParseNode node) @@ -46,4 +46,4 @@ public static OpenApiLicense LoadLicense(ParseNode node) return license; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs index 838c4e892..785ed6ee7 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiOperationDeserializer.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.Collections.Generic; using System.Linq; using Microsoft.OpenApi.Extensions; @@ -24,7 +23,6 @@ internal static partial class OpenApiV2Deserializer valueNode => LoadTagByReference( valueNode.Context, - valueNode.Diagnostic, valueNode.GetScalarValue())) }, { @@ -90,7 +88,7 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _operationPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; private static readonly FixedFieldMap _responsesFixedFields = @@ -100,7 +98,7 @@ internal static partial class OpenApiV2Deserializer new PatternFieldMap { {s => !s.StartsWith("x-"), (o, p, n) => o.Add(p, LoadResponse(n))}, - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; internal static OpenApiOperation LoadOperation(ParseNode node) @@ -131,7 +129,15 @@ internal static OpenApiOperation LoadOperation(ParseNode node) operation.RequestBody = CreateFormBody(node.Context, formParameters); } } - + + foreach (var response in operation.Responses.Values) + { + ProcessProduces(node.CheckMapNode("responses"), response, node.Context); + } + + // Reset so that it's not picked up later + node.Context.SetTempStorage(TempStorageKeys.OperationProduces, null); + return operation; } @@ -163,10 +169,10 @@ private static OpenApiRequestBody CreateFormBody(ParsingContext context, List(formParameters.Where(p => p.Required).Select(p => p.Name)) } }; - + var consumes = context.GetFromTempStorage>(TempStorageKeys.OperationConsumes) ?? context.GetFromTempStorage>(TempStorageKeys.GlobalConsumes) ?? - new List {"application/x-www-form-urlencoded"}; + new List { "application/x-www-form-urlencoded" }; var formBody = new OpenApiRequestBody { @@ -184,7 +190,7 @@ internal static OpenApiRequestBody CreateRequestBody( { var consumes = context.GetFromTempStorage>(TempStorageKeys.OperationConsumes) ?? context.GetFromTempStorage>(TempStorageKeys.GlobalConsumes) ?? - new List {"application/json"}; + new List { "application/json" }; var requestBody = new OpenApiRequestBody { @@ -203,7 +209,6 @@ internal static OpenApiRequestBody CreateRequestBody( private static OpenApiTag LoadTagByReference( ParsingContext context, - OpenApiDiagnostic diagnostic, string tagName) { var tagObject = new OpenApiTag() @@ -215,4 +220,4 @@ private static OpenApiTag LoadTagByReference( return tagObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs index b5f459a34..5be08c71e 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiParameterDeserializer.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using Microsoft.OpenApi.Any; +using System.Globalization; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; @@ -16,8 +16,6 @@ namespace Microsoft.OpenApi.Readers.V2 /// internal static partial class OpenApiV2Deserializer { - private static ParameterLocation? _in; - private static readonly FixedFieldMap _parameterFixedFields = new FixedFieldMap { @@ -57,12 +55,6 @@ internal static partial class OpenApiV2Deserializer o.AllowEmptyValue = bool.Parse(n.GetScalarValue()); } }, - { - "example", (o, n) => - { - o.Example = n.CreateAny(); - } - }, { "type", (o, n) => { @@ -90,25 +82,25 @@ internal static partial class OpenApiV2Deserializer { "minimum", (o, n) => { - GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maximum", (o, n) => { - GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maxLength", (o, n) => { - GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue()); + GetOrCreateSchema(o).MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -146,7 +138,41 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _parameterPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} + }; + + private static readonly AnyFieldMap _parameterAnyFields = + new AnyFieldMap + { + { + OpenApiConstants.Default, + new AnyFieldMapParameter( + p => p.Schema?.Default, + (p, v) => { + if (p.Schema != null || v != null) + { + GetOrCreateSchema(p).Default = v; + } + }, + p => p.Schema) + } + }; + + private static readonly AnyListFieldMap _parameterAnyListFields = + new AnyListFieldMap + { + { + OpenApiConstants.Enum, + new AnyListFieldMapParameter( + p => p.Schema?.Enum, + (p, v) => { + if (p.Schema != null || v != null && v.Count > 0) + { + GetOrCreateSchema(p).Enum = v; + } + }, + p => p.Schema) + }, }; private static void LoadStyle(OpenApiParameter p, string v) @@ -204,9 +230,11 @@ private static void ProcessIn(OpenApiParameter o, ParseNode n) switch (value) { case "body": + n.Context.SetTempStorage(TempStorageKeys.ParameterIsBodyOrFormData, true); n.Context.SetTempStorage(TempStorageKeys.BodyParameter, o); break; case "formData": + n.Context.SetTempStorage(TempStorageKeys.ParameterIsBodyOrFormData, true); var formParameters = n.Context.GetFromTempStorage>("formParameters"); if (formParameters == null) { @@ -216,9 +244,13 @@ private static void ProcessIn(OpenApiParameter o, ParseNode n) formParameters.Add(o); break; + case "query": + case "header": + case "path": + o.In = value.GetEnumFromDisplayName(); + break; default: - _in = value.GetEnumFromDisplayName(); - o.In = _in; + o.In = null; break; } } @@ -228,10 +260,10 @@ public static OpenApiParameter LoadParameter(ParseNode node) return LoadParameter(node, false); } - public static OpenApiParameter LoadParameter(ParseNode node, bool evenBody) + public static OpenApiParameter LoadParameter(ParseNode node, bool loadRequestBody) { // Reset the local variables every time this method is called. - _in = null; + node.Context.SetTempStorage(TempStorageKeys.ParameterIsBodyOrFormData, false); var mapNode = node.CheckMapNode("parameter"); @@ -241,11 +273,14 @@ public static OpenApiParameter LoadParameter(ParseNode node, bool evenBody) { return mapNode.GetReferencedObject(ReferenceType.Parameter, pointer); } - + var parameter = new OpenApiParameter(); ParseMap(mapNode, parameter, _parameterFixedFields, _parameterPatternFields); + ProcessAnyFields(mapNode, parameter, _parameterAnyFields); + ProcessAnyListFields(mapNode, parameter, _parameterAnyListFields); + var schema = node.Context.GetFromTempStorage("schema"); if (schema != null) { @@ -253,12 +288,18 @@ public static OpenApiParameter LoadParameter(ParseNode node, bool evenBody) node.Context.SetTempStorage("schema", null); } - if (_in == null && !evenBody) + bool isBodyOrFormData = (bool)node.Context.GetFromTempStorage(TempStorageKeys.ParameterIsBodyOrFormData); + if (isBodyOrFormData && !loadRequestBody) + { + return null; // Don't include Form or Body parameters when normal parameters are loaded. + } + + if (loadRequestBody && !isBodyOrFormData) { - return null; // Don't include Form or Body parameters in OpenApiOperation.Parameters list + return null; // Don't include non-Body or non-Form parameters when request bodies are loaded. } return parameter; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiPathItemDeserializer.cs index 19c806cbf..d8ff7b961 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiPathItemDeserializer.cs @@ -39,7 +39,7 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _pathItemPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())}, + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))}, }; public static OpenApiPathItem LoadPathItem(ParseNode node) @@ -53,4 +53,4 @@ public static OpenApiPathItem LoadPathItem(ParseNode node) return pathItem; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiPathsDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiPathsDeserializer.cs index d11843e8d..2aa5de979 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiPathsDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiPathsDeserializer.cs @@ -18,7 +18,7 @@ internal static partial class OpenApiV2Deserializer private static PatternFieldMap _pathsPatternFields = new PatternFieldMap { {s => s.StartsWith("/"), (o, k, n) => o.Add(k, LoadPathItem(n))}, - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiPaths LoadPaths(ParseNode node) @@ -32,4 +32,4 @@ public static OpenApiPaths LoadPaths(ParseNode node) return domainObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs index 7fc5170ec..343dcd2ce 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiResponseDeserializer.cs @@ -37,7 +37,7 @@ internal static partial class OpenApiV2Deserializer { "schema", (o, n) => { - n.Context.SetTempStorage(TempStorageKeys.ResponseSchema, LoadSchema(n)); + n.Context.SetTempStorage(TempStorageKeys.ResponseSchema, LoadSchema(n), o); } }, }; @@ -45,23 +45,62 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _responsePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; - private static void ProcessProduces(OpenApiResponse response, ParsingContext context) + private static readonly AnyFieldMap _mediaTypeAnyFields = + new AnyFieldMap + { + { + OpenApiConstants.Example, + new AnyFieldMapParameter( + m => m.Example, + (m, v) => m.Example = v, + m => m.Schema) + } + }; + + private static void ProcessProduces(MapNode mapNode, OpenApiResponse response, ParsingContext context) { - var produces = context.GetFromTempStorage>(TempStorageKeys.OperationProduces) ?? - context.GetFromTempStorage>(TempStorageKeys.GlobalProduces) ?? new List(); + if (response.Content == null) + { + response.Content = new Dictionary(); + } + else if (context.GetFromTempStorage(TempStorageKeys.ResponseProducesSet, response)) + { + // Process "produces" only once since once specified at operation level it cannot be overriden. + return; + } - response.Content = new Dictionary(); - foreach (var produce in produces) + var produces = context.GetFromTempStorage>(TempStorageKeys.OperationProduces) + ?? context.GetFromTempStorage>(TempStorageKeys.GlobalProduces); + if (produces != null) { - var mediaType = new OpenApiMediaType + foreach (var produce in produces) { - Schema = context.GetFromTempStorage(TempStorageKeys.ResponseSchema) - }; + var schema = context.GetFromTempStorage(TempStorageKeys.ResponseSchema, response); - response.Content.Add(produce, mediaType); + if (response.Content.ContainsKey(produce) && response.Content[produce] != null) + { + if (schema != null) + { + response.Content[produce].Schema = schema; + ProcessAnyFields(mapNode, response.Content[produce], _mediaTypeAnyFields); + } + } + else + { + var mediaType = new OpenApiMediaType + { + Schema = schema + }; + + response.Content.Add(produce, mediaType); + } + } + + context.SetTempStorage(TempStorageKeys.ResponseSchema, null, response); + context.SetTempStorage(TempStorageKeys.ResponseProducesSet, true, response); } } @@ -82,6 +121,7 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars { response.Content = new Dictionary(); } + OpenApiMediaType mediaTypeObject; if (response.Content.ContainsKey(mediaType)) { @@ -89,16 +129,18 @@ private static void LoadExample(OpenApiResponse response, string mediaType, Pars } else { - mediaTypeObject = new OpenApiMediaType(); + mediaTypeObject = new OpenApiMediaType + { + Schema = node.Context.GetFromTempStorage(TempStorageKeys.ResponseSchema, response) + }; response.Content.Add(mediaType, mediaTypeObject); } + mediaTypeObject.Example = exampleNode; } public static OpenApiResponse LoadResponse(ParseNode node) { - node.Context.SetTempStorage(TempStorageKeys.ResponseSchema, null); - var mapNode = node.CheckMapNode("response"); var pointer = mapNode.GetReferencePointer(); @@ -113,9 +155,15 @@ public static OpenApiResponse LoadResponse(ParseNode node) property.ParseField(response, _responseFixedFields, _responsePatternFields); } - ProcessProduces(response, node.Context); + foreach (var mediaType in response.Content.Values) + { + if (mediaType.Schema != null) + { + ProcessAnyFields(mapNode, mediaType, _mediaTypeAnyFields); + } + } return response; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs index 79f878463..41c40e497 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiSchemaDeserializer.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using Microsoft.OpenApi.Any; +using System.Collections.Generic; +using System.Globalization; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; -using System.Collections.Generic; namespace Microsoft.OpenApi.Readers.V2 { @@ -26,13 +26,13 @@ internal static partial class OpenApiV2Deserializer { "multipleOf", (o, n) => { - o.MultipleOf = decimal.Parse(n.GetScalarValue()); + o.MultipleOf = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maximum", (o, n) => { - o.Maximum = decimal.Parse(n.GetScalarValue()); + o.Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -44,7 +44,7 @@ internal static partial class OpenApiV2Deserializer { "minimum", (o, n) => { - o.Minimum = decimal.Parse(n.GetScalarValue()); + o.Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -56,13 +56,13 @@ internal static partial class OpenApiV2Deserializer { "maxLength", (o, n) => { - o.MaxLength = int.Parse(n.GetScalarValue()); + o.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - o.MinLength = int.Parse(n.GetScalarValue()); + o.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -74,13 +74,13 @@ internal static partial class OpenApiV2Deserializer { "maxItems", (o, n) => { - o.MaxItems = int.Parse(n.GetScalarValue()); + o.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minItems", (o, n) => { - o.MinItems = int.Parse(n.GetScalarValue()); + o.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -92,13 +92,13 @@ internal static partial class OpenApiV2Deserializer { "maxProperties", (o, n) => { - o.MaxProperties = int.Parse(n.GetScalarValue()); + o.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minProperties", (o, n) => { - o.MinProperties = int.Parse(n.GetScalarValue()); + o.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -206,7 +206,35 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _schemaPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} + }; + + private static readonly AnyFieldMap _schemaAnyFields = new AnyFieldMap + { + { + OpenApiConstants.Default, + new AnyFieldMapParameter( + s => s.Default, + (s, v) => s.Default = v, + s => s) + }, + { + OpenApiConstants.Example, + new AnyFieldMapParameter( + s => s.Example, + (s, v) => s.Example = v, + s => s) } + }; + + private static readonly AnyListFieldMap _schemaAnyListFields = new AnyListFieldMap + { + { + OpenApiConstants.Enum, + new AnyListFieldMapParameter( + s => s.Enum, + (s, v) => s.Enum = v, + s => s) + } }; public static OpenApiSchema LoadSchema(ParseNode node) @@ -219,14 +247,17 @@ public static OpenApiSchema LoadSchema(ParseNode node) return mapNode.GetReferencedObject(ReferenceType.Schema, pointer); } - var domainObject = new OpenApiSchema(); + var schema = new OpenApiSchema(); foreach (var propertyNode in mapNode) { - propertyNode.ParseField(domainObject, _schemaFixedFields, _schemaPatternFields); + propertyNode.ParseField(schema, _schemaFixedFields, _schemaPatternFields); } - return domainObject; + ProcessAnyFields(mapNode, schema, _schemaAnyFields); + ProcessAnyListFields(mapNode, schema, _schemaAnyListFields); + + return schema; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs index 2293c046a..c5e0776ee 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiSecurityRequirementDeserializer.cs @@ -22,7 +22,6 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) { var scheme = LoadSecuritySchemeByReference( mapNode.Context, - mapNode.Diagnostic, property.Name); var scopes = property.Value.CreateSimpleList(n2 => n2.GetScalarValue()); @@ -33,8 +32,8 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) } else { - node.Diagnostic.Errors.Add( - new OpenApiReaderError(node.Context, + mapNode.Context.Diagnostic.Errors.Add( + new OpenApiError(node.Context.GetLocation(), $"Scheme {property.Name} is not found")); } } @@ -44,7 +43,6 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) private static OpenApiSecurityScheme LoadSecuritySchemeByReference( ParsingContext context, - OpenApiDiagnostic diagnostic, string schemeName) { var securitySchemeObject = new OpenApiSecurityScheme() @@ -60,4 +58,4 @@ private static OpenApiSecurityScheme LoadSecuritySchemeByReference( return securitySchemeObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiSecuritySchemeDeserializer.cs index 5b88f33a8..7e0c6c1dc 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiSecuritySchemeDeserializer.cs @@ -77,7 +77,7 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _securitySchemePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiSecurityScheme LoadSecurityScheme(ParseNode node) @@ -128,4 +128,4 @@ public static OpenApiSecurityScheme LoadSecurityScheme(ParseNode node) return securityScheme; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiTagDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiTagDeserializer.cs index 636a06796..12fae8660 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiTagDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiTagDeserializer.cs @@ -37,7 +37,7 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _tagPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiTag LoadTag(ParseNode n) @@ -54,4 +54,4 @@ public static OpenApiTag LoadTag(ParseNode n) return domainObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2Deserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2Deserializer.cs index b611186c0..4bb15a8d9 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2Deserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2Deserializer.cs @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; namespace Microsoft.OpenApi.Readers.V2 @@ -33,9 +36,132 @@ private static void ParseMap( } } + private static void ProcessAnyFields( + MapNode mapNode, + T domainObject, + AnyFieldMap anyFieldMap) + { + foreach (var anyFieldName in anyFieldMap.Keys.ToList()) + { + try + { + mapNode.Context.StartObject(anyFieldName); + + var convertedOpenApiAny = OpenApiAnyConverter.GetSpecificOpenApiAny( + anyFieldMap[anyFieldName].PropertyGetter(domainObject), + anyFieldMap[anyFieldName].SchemaGetter(domainObject)); + + anyFieldMap[anyFieldName].PropertySetter(domainObject, convertedOpenApiAny); + } + catch (OpenApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new OpenApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + + private static void ProcessAnyListFields( + MapNode mapNode, + T domainObject, + AnyListFieldMap anyListFieldMap) + { + foreach (var anyListFieldName in anyListFieldMap.Keys.ToList()) + { + try + { + var newProperty = new List(); + + mapNode.Context.StartObject(anyListFieldName); + + var list = anyListFieldMap[anyListFieldName].PropertyGetter(domainObject); + if (list != null) + { + foreach (var propertyElement in list) + { + newProperty.Add( + OpenApiAnyConverter.GetSpecificOpenApiAny( + propertyElement, + anyListFieldMap[anyListFieldName].SchemaGetter(domainObject))); + } + } + + anyListFieldMap[anyListFieldName].PropertySetter(domainObject, newProperty); + } + catch (OpenApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new OpenApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + + private static void ProcessAnyMapFields( + MapNode mapNode, + T domainObject, + AnyMapFieldMap anyMapFieldMap) + { + foreach (var anyMapFieldName in anyMapFieldMap.Keys.ToList()) + { + try + { + var newProperty = new List(); + + mapNode.Context.StartObject(anyMapFieldName); + + foreach (var propertyMapElement in anyMapFieldMap[anyMapFieldName].PropertyMapGetter(domainObject)) + { + if (propertyMapElement.Value != null) + { + mapNode.Context.StartObject(propertyMapElement.Key); + + var any = anyMapFieldMap[anyMapFieldName].PropertyGetter(propertyMapElement.Value); + + var newAny = OpenApiAnyConverter.GetSpecificOpenApiAny( + any, + anyMapFieldMap[anyMapFieldName].SchemaGetter(domainObject)); + + anyMapFieldMap[anyMapFieldName].PropertySetter(propertyMapElement.Value, newAny); + } + } + } + catch (OpenApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new OpenApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + public static IOpenApiAny LoadAny(ParseNode node) { - return node.CreateAny(); + return OpenApiAnyConverter.GetSpecificOpenApiAny(node.CreateAny()); + } + + private static IOpenApiExtension LoadExtension(string name, ParseNode node) + { + if (node.Context.ExtensionParsers.TryGetValue(name, out var parser)) + { + return parser( + OpenApiAnyConverter.GetSpecificOpenApiAny(node.CreateAny()), + OpenApiSpecVersion.OpenApi2_0); + } + else + { + return OpenApiAnyConverter.GetSpecificOpenApiAny(node.CreateAny()); + } } private static string LoadString(ParseNode node) @@ -43,4 +169,4 @@ private static string LoadString(ParseNode node) return node.GetScalarValue(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs index 37708a574..99dfcd225 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs @@ -7,6 +7,7 @@ using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Exceptions; using Microsoft.OpenApi.Readers.Interface; using Microsoft.OpenApi.Readers.ParseNodes; using Microsoft.OpenApi.Readers.Properties; @@ -58,7 +59,7 @@ private static OpenApiReference ParseLocalReference(string localReference) var id = localReference.Substring( segments[0].Length + "/".Length + segments[1].Length + "/".Length); - return new OpenApiReference {Type = referenceType, Id = id}; + return new OpenApiReference { Type = referenceType, Id = id }; } throw new OpenApiException( @@ -90,7 +91,7 @@ private static ReferenceType ParseReferenceType(string referenceTypeName) return ReferenceType.SecurityScheme; default: - throw new ArgumentException(); + throw new OpenApiReaderException($"Unknown reference type '{referenceTypeName}'"); } } @@ -178,4 +179,4 @@ public T LoadElement(ParseNode node) where T : IOpenApiElement return (T)_loaders[typeof(T)](node); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiXmlDeserializer.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiXmlDeserializer.cs index 75c7d4658..ac7db2db6 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiXmlDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiXmlDeserializer.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. using System; +using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Exceptions; using Microsoft.OpenApi.Readers.ParseNodes; namespace Microsoft.OpenApi.Readers.V2 @@ -25,7 +27,14 @@ internal static partial class OpenApiV2Deserializer { "namespace", (o, n) => { - o.Namespace = new Uri(n.GetScalarValue(), UriKind.Absolute); + if (Uri.IsWellFormedUriString(n.GetScalarValue(), UriKind.Absolute)) + { + o.Namespace = new Uri(n.GetScalarValue(), UriKind.Absolute); + } + else + { + throw new OpenApiReaderException($"Xml Namespace requires absolute URL. '{n.GetScalarValue()}' is not valid."); + } } }, { @@ -51,7 +60,7 @@ internal static partial class OpenApiV2Deserializer private static readonly PatternFieldMap _xmlPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiXml LoadXml(ParseNode node) @@ -67,4 +76,4 @@ public static OpenApiXml LoadXml(ParseNode node) return xml; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V2/TempStorageKeys.cs b/src/Microsoft.OpenApi.Readers/V2/TempStorageKeys.cs index ef1bbf219..5a216e086 100644 --- a/src/Microsoft.OpenApi.Readers/V2/TempStorageKeys.cs +++ b/src/Microsoft.OpenApi.Readers/V2/TempStorageKeys.cs @@ -12,8 +12,10 @@ internal static class TempStorageKeys public const string BodyParameter = "bodyParameter"; public const string FormParameters = "formParameters"; public const string OperationProduces = "operationProduces"; + public const string ResponseProducesSet = "responseProducesSet"; public const string OperationConsumes = "operationConsumes"; public const string GlobalConsumes = "globalConsumes"; public const string GlobalProduces = "globalProduces"; + public const string ParameterIsBodyOrFormData = "parameterIsBodyOrFormData"; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiCallbackDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiCallbackDeserializer.cs index ddc9d3d78..af1376580 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiCallbackDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiCallbackDeserializer.cs @@ -20,8 +20,8 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _callbackPatternFields = new PatternFieldMap { - {s => s.StartsWith("$"), (o, p, n) => o.AddPathItem(RuntimeExpression.Build(p), LoadPathItem(n))}, - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())}, + {s => !s.StartsWith("x-"), (o, p, n) => o.AddPathItem(RuntimeExpression.Build(p), LoadPathItem(n))}, + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))}, }; public static OpenApiCallback LoadCallback(ParseNode node) @@ -41,4 +41,4 @@ public static OpenApiCallback LoadCallback(ParseNode node) return domainObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiComponentsDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiComponentsDeserializer.cs index 018618efd..30d711d33 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiComponentsDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiComponentsDeserializer.cs @@ -34,7 +34,7 @@ internal static partial class OpenApiV3Deserializer private static PatternFieldMap _componentsPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiComponents LoadComponents(ParseNode node) @@ -47,4 +47,4 @@ public static OpenApiComponents LoadComponents(ParseNode node) return components; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiContactDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiContactDeserializer.cs index 2d873461a..151a12354 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiContactDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiContactDeserializer.cs @@ -38,7 +38,7 @@ internal static partial class OpenApiV3Deserializer private static PatternFieldMap _contactPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiContact LoadContact(ParseNode node) @@ -51,4 +51,4 @@ public static OpenApiContact LoadContact(ParseNode node) return contact; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiDiscriminatorDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiDiscriminatorDeserializer.cs index 8978e4238..867057d32 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiDiscriminatorDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiDiscriminatorDeserializer.cs @@ -45,4 +45,4 @@ public static OpenApiDiscriminator LoadDiscriminator(ParseNode node) return discriminator; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs index 220164c13..df1434cd9 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs @@ -44,7 +44,7 @@ internal static partial class OpenApiV3Deserializer private static PatternFieldMap _openApiPatternFields = new PatternFieldMap { // We have no semantics to verify X- nodes, therefore treat them as just values. - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p, n))} }; public static OpenApiDocument LoadOpenApi(RootNode rootNode) @@ -57,17 +57,5 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode) return openApidoc; } - - - public static IOpenApiExtension LoadExtension(string name, ParseNode node) - { - if (node.Context.ExtensionParsers.TryGetValue(name, out var parser)) { - return parser(node.CreateAny()); - } - else - { - return node.CreateAny(); - } - } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiEncodingDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiEncodingDeserializer.cs index 279b3c0d4..b3bda4b61 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiEncodingDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiEncodingDeserializer.cs @@ -59,7 +59,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _encodingPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiEncoding LoadEncoding(ParseNode node) @@ -75,4 +75,4 @@ public static OpenApiEncoding LoadEncoding(ParseNode node) return encoding; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiExampleDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiExampleDeserializer.cs index e204f034d..1e114ad73 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiExampleDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiExampleDeserializer.cs @@ -45,7 +45,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _examplePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiExample LoadExample(ParseNode node) @@ -67,4 +67,4 @@ public static OpenApiExample LoadExample(ParseNode node) return example; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiExternalDocsDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiExternalDocsDeserializer.cs index ec2c16ff1..2fef79b6a 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiExternalDocsDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiExternalDocsDeserializer.cs @@ -45,4 +45,4 @@ public static OpenApiExternalDocs LoadExternalDocs(ParseNode node) return externalDocs; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiHeaderDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiHeaderDeserializer.cs index 868330a8f..1616d67f0 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiHeaderDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiHeaderDeserializer.cs @@ -33,6 +33,12 @@ internal static partial class OpenApiV3Deserializer o.Deprecated = bool.Parse(n.GetScalarValue()); } }, + { + "allowEmptyValue", (o, n) => + { + o.AllowEmptyValue = bool.Parse(n.GetScalarValue()); + } + }, { "allowReserved", (o, n) => { @@ -45,17 +51,35 @@ internal static partial class OpenApiV3Deserializer o.Style = n.GetScalarValue().GetEnumFromDisplayName(); } }, + { + "explode", (o, n) => + { + o.Explode = bool.Parse(n.GetScalarValue()); + } + }, { "schema", (o, n) => { o.Schema = LoadSchema(n); } - } + }, + { + "examples", (o, n) => + { + o.Examples = n.CreateMap(LoadExample); + } + }, + { + "example", (o, n) => + { + o.Example = n.CreateAny(); + } + }, }; private static readonly PatternFieldMap _headerPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiHeader LoadHeader(ParseNode node) @@ -77,4 +101,4 @@ public static OpenApiHeader LoadHeader(ParseNode node) return header; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiInfoDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiInfoDeserializer.cs index 4a65f261d..d5de92852 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiInfoDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiInfoDeserializer.cs @@ -57,7 +57,7 @@ internal static partial class OpenApiV3Deserializer public static PatternFieldMap InfoPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, k, n) => o.Extensions.Add(k,LoadExtension(k, n))} + {s => s.StartsWith("x-"), (o, k, n) => o.AddExtension(k,LoadExtension(k, n))} }; public static OpenApiInfo LoadInfo(ParseNode node) @@ -65,11 +65,11 @@ public static OpenApiInfo LoadInfo(ParseNode node) var mapNode = node.CheckMapNode("Info"); var info = new OpenApiInfo(); - var required = new List {"title", "version"}; + var required = new List { "title", "version" }; ParseMap(mapNode, info, InfoFixedFields, InfoPatternFields); return info; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiLicenseDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiLicenseDeserializer.cs index 35e11c243..3c38d8b9a 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiLicenseDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiLicenseDeserializer.cs @@ -32,7 +32,7 @@ internal static partial class OpenApiV3Deserializer private static PatternFieldMap _licensePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; internal static OpenApiLicense LoadLicense(ParseNode node) @@ -46,4 +46,4 @@ internal static OpenApiLicense LoadLicense(ParseNode node) return license; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiLinkDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiLinkDeserializer.cs index c2a205089..7bf4c650b 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiLinkDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiLinkDeserializer.cs @@ -50,7 +50,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _linkPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())}, + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))}, }; public static OpenApiLink LoadLink(ParseNode node) @@ -69,4 +69,4 @@ public static OpenApiLink LoadLink(ParseNode node) return link; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiMediaTypeDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiMediaTypeDeserializer.cs index 983d67848..695f1cc1b 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiMediaTypeDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiMediaTypeDeserializer.cs @@ -1,7 +1,10 @@ // 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.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; @@ -18,35 +21,65 @@ internal static partial class OpenApiV3Deserializer new FixedFieldMap { { - "schema", (o, n) => + OpenApiConstants.Schema, (o, n) => { o.Schema = LoadSchema(n); } }, { - "examples", (o, n) => + OpenApiConstants.Examples, (o, n) => { o.Examples = n.CreateMap(LoadExample); } }, { - "example", (o, n) => + OpenApiConstants.Example, (o, n) => { o.Example = n.CreateAny(); } }, - //Encoding + { + OpenApiConstants.Encoding, (o, n) => + { + o.Encoding = n.CreateMap(LoadEncoding); + } + }, }; private static readonly PatternFieldMap _mediaTypePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; + private static readonly AnyFieldMap _mediaTypeAnyFields = new AnyFieldMap + { + { + OpenApiConstants.Example, + new AnyFieldMapParameter( + s => s.Example, + (s, v) => s.Example = v, + s => s.Schema) + } + }; + + + private static readonly AnyMapFieldMap _mediaTypeAnyMapOpenApiExampleFields = + new AnyMapFieldMap + { + { + OpenApiConstants.Examples, + new AnyMapFieldMapParameter( + m => m.Examples, + e => e.Value, + (e, v) => e.Value = v, + m => m.Schema) + } + }; + public static OpenApiMediaType LoadMediaType(ParseNode node) { - var mapNode = node.CheckMapNode("content"); + var mapNode = node.CheckMapNode(OpenApiConstants.Content); if (!mapNode.Any()) { @@ -57,7 +90,10 @@ public static OpenApiMediaType LoadMediaType(ParseNode node) ParseMap(mapNode, mediaType, _mediaTypeFixedFields, _mediaTypePatternFields); + ProcessAnyFields(mapNode, mediaType, _mediaTypeAnyFields); + ProcessAnyMapFields(mapNode, mediaType, _mediaTypeAnyMapOpenApiExampleFields); + return mediaType; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowDeserializer.cs index 96cd9af8d..2653ce631 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowDeserializer.cs @@ -41,7 +41,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _oAuthFlowPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiOAuthFlow LoadOAuthFlow(ParseNode node) @@ -57,4 +57,4 @@ public static OpenApiOAuthFlow LoadOAuthFlow(ParseNode node) return oauthFlow; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowsDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowsDeserializer.cs index 5d88a9175..bd19f2716 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowsDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiOAuthFlowsDeserializer.cs @@ -25,7 +25,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _oAuthFlowsPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiOAuthFlows LoadOAuthFlows(ParseNode node) @@ -41,4 +41,4 @@ public static OpenApiOAuthFlows LoadOAuthFlows(ParseNode node) return oAuthFlows; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiOperationDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiOperationDeserializer.cs index 579063278..16f7a16f4 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiOperationDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiOperationDeserializer.cs @@ -21,7 +21,6 @@ internal static partial class OpenApiV3Deserializer valueNode => LoadTagByReference( valueNode.Context, - valueNode.Diagnostic, valueNode.GetScalarValue())) }, { @@ -95,7 +94,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _operationPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())}, + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))}, }; internal static OpenApiOperation LoadOperation(ParseNode node) @@ -111,7 +110,6 @@ internal static OpenApiOperation LoadOperation(ParseNode node) private static OpenApiTag LoadTagByReference( ParsingContext context, - OpenApiDiagnostic diagnostic, string tagName) { var tagObject = new OpenApiTag() @@ -123,8 +121,8 @@ private static OpenApiTag LoadTagByReference( Id = tagName } }; - + return tagObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiParameterDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiParameterDeserializer.cs index e713d00b7..e8fad07a5 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiParameterDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiParameterDeserializer.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System.Collections.Generic; +using System; using System.Linq; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; @@ -27,7 +27,18 @@ internal static partial class OpenApiV3Deserializer { "in", (o, n) => { - o.In = n.GetScalarValue().GetEnumFromDisplayName(); + var inString = n.GetScalarValue(); + + if ( Enum.GetValues(typeof(ParameterLocation)).Cast() + .Select( e => e.GetDisplayName() ) + .Contains(inString) ) + { + o.In = n.GetScalarValue().GetEnumFromDisplayName(); + } + else + { + o.In = null; + } } }, { @@ -101,9 +112,33 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _parameterPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; + private static readonly AnyFieldMap _parameterAnyFields = new AnyFieldMap + { + { + OpenApiConstants.Example, + new AnyFieldMapParameter( + s => s.Example, + (s, v) => s.Example = v, + s => s.Schema) + } + }; + + private static readonly AnyMapFieldMap _parameterAnyMapOpenApiExampleFields = + new AnyMapFieldMap + { + { + OpenApiConstants.Examples, + new AnyMapFieldMapParameter( + m => m.Examples, + e => e.Value, + (e, v) => e.Value = v, + m => m.Schema) + } + }; + public static OpenApiParameter LoadParameter(ParseNode node) { var mapNode = node.CheckMapNode("parameter"); @@ -115,11 +150,12 @@ public static OpenApiParameter LoadParameter(ParseNode node) } var parameter = new OpenApiParameter(); - var required = new List {"name", "in"}; ParseMap(mapNode, parameter, _parameterFixedFields, _parameterPatternFields); + ProcessAnyFields(mapNode, parameter, _parameterAnyFields); + ProcessAnyMapFields(mapNode, parameter, _parameterAnyMapOpenApiExampleFields); return parameter; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiPathItemDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiPathItemDeserializer.cs index ca6c0d73e..70f53b83a 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiPathItemDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiPathItemDeserializer.cs @@ -43,7 +43,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _pathItemPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiPathItem LoadPathItem(ParseNode node) @@ -57,4 +57,4 @@ public static OpenApiPathItem LoadPathItem(ParseNode node) return pathItem; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiPathsDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiPathsDeserializer.cs index 207c5228b..fcfad096c 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiPathsDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiPathsDeserializer.cs @@ -18,7 +18,7 @@ internal static partial class OpenApiV3Deserializer private static PatternFieldMap _pathsPatternFields = new PatternFieldMap { {s => s.StartsWith("/"), (o, k, n) => o.Add(k, LoadPathItem(n))}, - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiPaths LoadPaths(ParseNode node) @@ -32,4 +32,4 @@ public static OpenApiPaths LoadPaths(ParseNode node) return domainObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiRequestBodyDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiRequestBodyDeserializer.cs index 15b53473e..a2633028e 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiRequestBodyDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiRequestBodyDeserializer.cs @@ -39,7 +39,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _requestBodyPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiRequestBody LoadRequestBody(ParseNode node) @@ -61,4 +61,4 @@ public static OpenApiRequestBody LoadRequestBody(ParseNode node) return requestBody; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiResponseDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiResponseDeserializer.cs index e5c483a77..70ea0c9bf 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiResponseDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiResponseDeserializer.cs @@ -45,7 +45,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _responsePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiResponse LoadResponse(ParseNode node) @@ -58,11 +58,11 @@ public static OpenApiResponse LoadResponse(ParseNode node) return mapNode.GetReferencedObject(ReferenceType.Response, pointer); } - var requiredFields = new List {"description"}; + var requiredFields = new List { "description" }; var response = new OpenApiResponse(); ParseMap(mapNode, response, _responseFixedFields, _responsePatternFields); return response; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiResponsesDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiResponsesDeserializer.cs index 9ee33015e..9fe4d075f 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiResponsesDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiResponsesDeserializer.cs @@ -18,7 +18,7 @@ internal static partial class OpenApiV3Deserializer public static PatternFieldMap ResponsesPatternFields = new PatternFieldMap { {s => !s.StartsWith("x-"), (o, p, n) => o.Add(p, LoadResponse(n))}, - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiResponses LoadResponses(ParseNode node) @@ -32,4 +32,4 @@ public static OpenApiResponses LoadResponses(ParseNode node) return domainObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs index da67820a3..87769c7c9 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs @@ -6,6 +6,7 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; using System.Collections.Generic; +using System.Globalization; namespace Microsoft.OpenApi.Readers.V3 { @@ -26,13 +27,13 @@ internal static partial class OpenApiV3Deserializer { "multipleOf", (o, n) => { - o.MultipleOf = decimal.Parse(n.GetScalarValue()); + o.MultipleOf = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "maximum", (o, n) => { - o.Maximum = decimal.Parse(n.GetScalarValue()); + o.Maximum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -44,7 +45,7 @@ internal static partial class OpenApiV3Deserializer { "minimum", (o, n) => { - o.Minimum = decimal.Parse(n.GetScalarValue()); + o.Minimum = decimal.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -56,13 +57,13 @@ internal static partial class OpenApiV3Deserializer { "maxLength", (o, n) => { - o.MaxLength = int.Parse(n.GetScalarValue()); + o.MaxLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minLength", (o, n) => { - o.MinLength = int.Parse(n.GetScalarValue()); + o.MinLength = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -74,13 +75,13 @@ internal static partial class OpenApiV3Deserializer { "maxItems", (o, n) => { - o.MaxItems = int.Parse(n.GetScalarValue()); + o.MaxItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minItems", (o, n) => { - o.MinItems = int.Parse(n.GetScalarValue()); + o.MinItems = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -92,13 +93,13 @@ internal static partial class OpenApiV3Deserializer { "maxProperties", (o, n) => { - o.MaxProperties = int.Parse(n.GetScalarValue()); + o.MaxProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { "minProperties", (o, n) => { - o.MinProperties = int.Parse(n.GetScalarValue()); + o.MinProperties = int.Parse(n.GetScalarValue(), CultureInfo.InvariantCulture); } }, { @@ -239,7 +240,36 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _schemaPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} + }; + + private static readonly AnyFieldMap _schemaAnyFields = new AnyFieldMap + { + { + OpenApiConstants.Default, + new AnyFieldMapParameter( + s => s.Default, + (s, v) => s.Default = v, + s => s) + }, + { + OpenApiConstants.Example, + new AnyFieldMapParameter( + s => s.Example, + (s, v) => s.Example = v, + s => s) + } + }; + + private static readonly AnyListFieldMap _schemaAnyListFields = new AnyListFieldMap + { + { + OpenApiConstants.Enum, + new AnyListFieldMapParameter( + s => s.Enum, + (s, v) => s.Enum = v, + s => s) + } }; public static OpenApiSchema LoadSchema(ParseNode node) @@ -250,21 +280,24 @@ public static OpenApiSchema LoadSchema(ParseNode node) if (pointer != null) { - return new OpenApiSchema() - { - UnresolvedReference = true, - Reference = node.Context.VersionService.ConvertToOpenApiReference(pointer,ReferenceType.Schema) - }; + return new OpenApiSchema() + { + UnresolvedReference = true, + Reference = node.Context.VersionService.ConvertToOpenApiReference(pointer, ReferenceType.Schema) + }; } - var domainObject = new OpenApiSchema(); + var schema = new OpenApiSchema(); foreach (var propertyNode in mapNode) { - propertyNode.ParseField(domainObject, _schemaFixedFields, _schemaPatternFields); + propertyNode.ParseField(schema, _schemaFixedFields, _schemaPatternFields); } - return domainObject; + ProcessAnyFields(mapNode, schema, _schemaAnyFields); + ProcessAnyListFields(mapNode, schema, _schemaAnyListFields); + + return schema; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs index 2567c7826..b6b80cf7b 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiSecurityRequirementDeserializer.cs @@ -22,7 +22,6 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) { var scheme = LoadSecuritySchemeByReference( mapNode.Context, - mapNode.Diagnostic, property.Name); var scopes = property.Value.CreateSimpleList(value => value.GetScalarValue()); @@ -33,8 +32,8 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) } else { - node.Diagnostic.Errors.Add( - new OpenApiReaderError(node.Context, $"Scheme {property.Name} is not found")); + mapNode.Context.Diagnostic.Errors.Add( + new OpenApiError(node.Context.GetLocation(), $"Scheme {property.Name} is not found")); } } @@ -43,7 +42,6 @@ public static OpenApiSecurityRequirement LoadSecurityRequirement(ParseNode node) private static OpenApiSecurityScheme LoadSecuritySchemeByReference( ParsingContext context, - OpenApiDiagnostic diagnostic, string schemeName) { var securitySchemeObject = new OpenApiSecurityScheme() @@ -59,4 +57,4 @@ private static OpenApiSecurityScheme LoadSecuritySchemeByReference( return securitySchemeObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiSecuritySchemeDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiSecuritySchemeDeserializer.cs index 6779750d1..0e7b1c39c 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiSecuritySchemeDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiSecuritySchemeDeserializer.cs @@ -70,7 +70,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _securitySchemePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiSecurityScheme LoadSecurityScheme(ParseNode node) @@ -86,4 +86,4 @@ public static OpenApiSecurityScheme LoadSecurityScheme(ParseNode node) return securityScheme; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiServerDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiServerDeserializer.cs index a1cc85b36..e278abc90 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiServerDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiServerDeserializer.cs @@ -37,7 +37,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _serverPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiServer LoadServer(ParseNode node) @@ -51,4 +51,4 @@ public static OpenApiServer LoadServer(ParseNode node) return server; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiServerVariableDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiServerVariableDeserializer.cs index d8fb35ef1..9b6acbc8d 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiServerVariableDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiServerVariableDeserializer.cs @@ -39,7 +39,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _serverVariablePatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiServerVariable LoadServerVariable(ParseNode node) @@ -53,4 +53,4 @@ public static OpenApiServerVariable LoadServerVariable(ParseNode node) return serverVariable; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiTagDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiTagDeserializer.cs index 8e8c329e3..6a987969b 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiTagDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiTagDeserializer.cs @@ -37,7 +37,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _tagPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiTag LoadTag(ParseNode n) @@ -54,4 +54,4 @@ public static OpenApiTag LoadTag(ParseNode n) return domainObject; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiV3Deserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiV3Deserializer.cs index 2ad61a9bb..9689c8fe1 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiV3Deserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiV3Deserializer.cs @@ -4,7 +4,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Expressions; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; @@ -34,6 +36,111 @@ private static void ParseMap( } + private static void ProcessAnyFields( + MapNode mapNode, + T domainObject, + AnyFieldMap anyFieldMap) + { + foreach (var anyFieldName in anyFieldMap.Keys.ToList()) + { + try + { + mapNode.Context.StartObject(anyFieldName); + + var convertedOpenApiAny = OpenApiAnyConverter.GetSpecificOpenApiAny( + anyFieldMap[anyFieldName].PropertyGetter(domainObject), + anyFieldMap[anyFieldName].SchemaGetter(domainObject)); + + anyFieldMap[anyFieldName].PropertySetter(domainObject, convertedOpenApiAny); + } + catch (OpenApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new OpenApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + + private static void ProcessAnyListFields( + MapNode mapNode, + T domainObject, + AnyListFieldMap anyListFieldMap) + { + foreach (var anyListFieldName in anyListFieldMap.Keys.ToList()) + { + try + { + var newProperty = new List(); + + mapNode.Context.StartObject(anyListFieldName); + + foreach (var propertyElement in anyListFieldMap[anyListFieldName].PropertyGetter(domainObject)) + { + newProperty.Add( + OpenApiAnyConverter.GetSpecificOpenApiAny( + propertyElement, + anyListFieldMap[anyListFieldName].SchemaGetter(domainObject))); + } + + anyListFieldMap[anyListFieldName].PropertySetter(domainObject, newProperty); + } + catch (OpenApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new OpenApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + + private static void ProcessAnyMapFields( + MapNode mapNode, + T domainObject, + AnyMapFieldMap anyMapFieldMap) + { + foreach (var anyMapFieldName in anyMapFieldMap.Keys.ToList()) + { + try + { + var newProperty = new List(); + + mapNode.Context.StartObject(anyMapFieldName); + + foreach (var propertyMapElement in anyMapFieldMap[anyMapFieldName].PropertyMapGetter(domainObject)) + { + mapNode.Context.StartObject(propertyMapElement.Key); + + if (propertyMapElement.Value != null) + { + var any = anyMapFieldMap[anyMapFieldName].PropertyGetter(propertyMapElement.Value); + + var newAny = OpenApiAnyConverter.GetSpecificOpenApiAny( + any, + anyMapFieldMap[anyMapFieldName].SchemaGetter(domainObject)); + + anyMapFieldMap[anyMapFieldName].PropertySetter(propertyMapElement.Value, newAny); + } + } + } + catch (OpenApiException exception) + { + exception.Pointer = mapNode.Context.GetLocation(); + mapNode.Context.Diagnostic.Errors.Add(new OpenApiError(exception)); + } + finally + { + mapNode.Context.EndObject(); + } + } + } + private static RuntimeExpression LoadRuntimeExpression(ParseNode node) { var value = node.GetScalarValue(); @@ -54,13 +161,27 @@ private static RuntimeExpressionAnyWrapper LoadRuntimeExpressionAnyWrapper(Parse return new RuntimeExpressionAnyWrapper { - Any = node.CreateAny() + Any = OpenApiAnyConverter.GetSpecificOpenApiAny(node.CreateAny()) }; } public static IOpenApiAny LoadAny(ParseNode node) { - return node.CreateAny(); + return OpenApiAnyConverter.GetSpecificOpenApiAny(node.CreateAny()); + } + + private static IOpenApiExtension LoadExtension(string name, ParseNode node) + { + if (node.Context.ExtensionParsers.TryGetValue(name, out var parser)) + { + return parser( + OpenApiAnyConverter.GetSpecificOpenApiAny(node.CreateAny()), + OpenApiSpecVersion.OpenApi3_0); + } + else + { + return OpenApiAnyConverter.GetSpecificOpenApiAny(node.CreateAny()); + } } private static string LoadString(ParseNode node) @@ -68,4 +189,4 @@ private static string LoadString(ParseNode node) return node.GetScalarValue(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs index 891822a21..1630e60e1 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs @@ -19,7 +19,8 @@ namespace Microsoft.OpenApi.Readers.V3 /// internal class OpenApiV3VersionService : IOpenApiVersionService { - private IDictionary> _loaders = new Dictionary> { + private IDictionary> _loaders = new Dictionary> + { [typeof(IOpenApiAny)] = OpenApiV3Deserializer.LoadAny, [typeof(OpenApiCallback)] = OpenApiV3Deserializer.LoadCallback, [typeof(OpenApiComponents)] = OpenApiV3Deserializer.LoadComponents, @@ -48,7 +49,7 @@ internal class OpenApiV3VersionService : IOpenApiVersionService [typeof(OpenApiTag)] = OpenApiV3Deserializer.LoadTag, [typeof(OpenApiXml)] = OpenApiV3Deserializer.LoadXml }; - + /// /// Parse the string to a object. /// @@ -108,7 +109,7 @@ public OpenApiDocument LoadDocument(RootNode rootNode) public T LoadElement(ParseNode node) where T : IOpenApiElement { - return (T)_loaders[typeof(T)](node); + return (T)_loaders[typeof(T)](node); } private OpenApiReference ParseLocalReference(string localReference) @@ -125,11 +126,11 @@ private OpenApiReference ParseLocalReference(string localReference) if (segments[1] == "components") { var referenceType = segments[2].GetEnumFromDisplayName(); - return new OpenApiReference {Type = referenceType, Id = segments[3]}; + return new OpenApiReference { Type = referenceType, Id = segments[3] }; } } throw new OpenApiException(string.Format(SRResource.ReferenceHasInvalidFormat, localReference)); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiXmlDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiXmlDeserializer.cs index 0ce7ea2eb..dc05da1c2 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiXmlDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiXmlDeserializer.cs @@ -51,7 +51,7 @@ internal static partial class OpenApiV3Deserializer private static readonly PatternFieldMap _xmlPatternFields = new PatternFieldMap { - {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())} + {s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, LoadExtension(p,n))} }; public static OpenApiXml LoadXml(ParseNode node) @@ -67,4 +67,4 @@ public static OpenApiXml LoadXml(ParseNode node) return xml; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Readers/YamlHelper.cs b/src/Microsoft.OpenApi.Readers/YamlHelper.cs index a283ea0d3..90794b080 100644 --- a/src/Microsoft.OpenApi.Readers/YamlHelper.cs +++ b/src/Microsoft.OpenApi.Readers/YamlHelper.cs @@ -31,4 +31,4 @@ public static YamlNode ParseYamlString(string yamlString) return yamlDocument.RootNode; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Tool/Microsoft.OpenApi.Tool.csproj b/src/Microsoft.OpenApi.Tool/Microsoft.OpenApi.Tool.csproj new file mode 100644 index 000000000..f8f1eab1c --- /dev/null +++ b/src/Microsoft.OpenApi.Tool/Microsoft.OpenApi.Tool.csproj @@ -0,0 +1,25 @@ + + + + Exe + netcoreapp3.1 + true + openapi + ./../../artifacts + 0.5.0 + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.OpenApi.Tool/OpenApiService.cs b/src/Microsoft.OpenApi.Tool/OpenApiService.cs new file mode 100644 index 000000000..7a8e8ced2 --- /dev/null +++ b/src/Microsoft.OpenApi.Tool/OpenApiService.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using Microsoft.OpenApi.Validations; +using Microsoft.OpenApi.Writers; + +namespace Microsoft.OpenApi.Tool +{ + static class OpenApiService + { + public static void ProcessOpenApiDocument( + FileInfo input, + FileInfo output, + OpenApiSpecVersion version, + OpenApiFormat format, + bool inline) + { + OpenApiDocument document; + using (Stream stream = input.OpenRead()) + { + + document = new OpenApiStreamReader(new OpenApiReaderSettings + { + ReferenceResolution = ReferenceResolutionSetting.ResolveLocalReferences, + RuleSet = ValidationRuleSet.GetDefaultRuleSet() + } + ).Read(stream, out var context); + if (context.Errors.Count != 0) + { + var errorReport = new StringBuilder(); + + foreach (var error in context.Errors) + { + errorReport.AppendLine(error.ToString()); + } + + throw new ArgumentException(String.Join(Environment.NewLine, context.Errors.Select(e => e.Message).ToArray())); + } + } + + using (var outputStream = output?.Create()) + { + TextWriter textWriter; + + if (outputStream!=null) + { + textWriter = new StreamWriter(outputStream); + } else + { + textWriter = Console.Out; + } + + var settings = new OpenApiWriterSettings() + { + ReferenceInline = inline == true ? ReferenceInlineSetting.InlineLocalReferences : ReferenceInlineSetting.DoNotInlineReferences + }; + IOpenApiWriter writer; + switch (format) + { + case OpenApiFormat.Json: + writer = new OpenApiJsonWriter(textWriter, settings); + break; + case OpenApiFormat.Yaml: + writer = new OpenApiYamlWriter(textWriter, settings); + break; + default: + throw new ArgumentException("Unknown format"); + } + + document.Serialize(writer,version ); + + textWriter.Flush(); + } + } +} +} diff --git a/src/Microsoft.OpenApi.Tool/Program.cs b/src/Microsoft.OpenApi.Tool/Program.cs new file mode 100644 index 000000000..3d229c411 --- /dev/null +++ b/src/Microsoft.OpenApi.Tool/Program.cs @@ -0,0 +1,30 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.IO; +using System.Threading.Tasks; +using Microsoft.OpenApi; + +namespace Microsoft.OpenApi.Tool +{ + class Program + { + static async Task Main(string[] args) + { + var command = new RootCommand + { + new Option("--input") { Argument = new Argument() }, + new Option("--output") { Argument = new Argument() }, + new Option("--version") { Argument = new Argument() }, + new Option("--format") { Argument = new Argument() }, + new Option("--inline") { Argument = new Argument() } + }; + + command.Handler = CommandHandler.Create( + OpenApiService.ProcessOpenApiDocument); + + // Parse the incoming args and invoke the handler + return await command.InvokeAsync(args); + } + } +} diff --git a/src/Microsoft.OpenApi.Workbench/App.xaml.cs b/src/Microsoft.OpenApi.Workbench/App.xaml.cs index 6b849bd1f..d3fc6dcb7 100644 --- a/src/Microsoft.OpenApi.Workbench/App.xaml.cs +++ b/src/Microsoft.OpenApi.Workbench/App.xaml.cs @@ -11,4 +11,4 @@ namespace Microsoft.OpenApi.Workbench public partial class App : Application { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Workbench/MainModel.cs b/src/Microsoft.OpenApi.Workbench/MainModel.cs index 087340f14..6304b7f23 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; @@ -176,16 +193,16 @@ internal void ParseDocument() { stream = CreateStream(_input); } - + var stopwatch = new Stopwatch(); stopwatch.Start(); var document = new OpenApiStreamReader(new OpenApiReaderSettings - { - ReferenceResolution = ReferenceResolutionSetting.ResolveLocalReferences, - RuleSet = ValidationRuleSet.GetDefaultRuleSet() - } + { + ReferenceResolution = ReferenceResolutionSetting.ResolveLocalReferences, + RuleSet = ValidationRuleSet.GetDefaultRuleSet() + } ).Read(stream, out var context); stopwatch.Stop(); ParseTime = $"{stopwatch.ElapsedMilliseconds} ms"; @@ -217,7 +234,7 @@ internal void ParseDocument() var walker = new OpenApiWalker(statsVisitor); walker.Walk(document); - Errors += Environment.NewLine + "Statistics:" + Environment.NewLine + statsVisitor.GetStatisticsReport(); + Errors += Environment.NewLine + "Statistics:" + Environment.NewLine + statsVisitor.GetStatisticsReport(); } catch (Exception ex) { @@ -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; @@ -259,4 +280,4 @@ private MemoryStream CreateStream(string text) /// public event PropertyChangedEventHandler PropertyChanged; } -} \ No newline at end of file +} 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.Workbench/MainWindow.xaml.cs b/src/Microsoft.OpenApi.Workbench/MainWindow.xaml.cs index 214232d04..f33132359 100644 --- a/src/Microsoft.OpenApi.Workbench/MainWindow.xaml.cs +++ b/src/Microsoft.OpenApi.Workbench/MainWindow.xaml.cs @@ -23,4 +23,4 @@ private void Button_Click(object sender, RoutedEventArgs e) _mainModel.ParseDocument(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi.Workbench/Properties/AssemblyInfo.cs b/src/Microsoft.OpenApi.Workbench/Properties/AssemblyInfo.cs index 6b1d07384..b9a41106f 100644 --- a/src/Microsoft.OpenApi.Workbench/Properties/AssemblyInfo.cs +++ b/src/Microsoft.OpenApi.Workbench/Properties/AssemblyInfo.cs @@ -33,11 +33,11 @@ [assembly: ThemeInfo( ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) + //(used if a resource is not found in the page, + // or application resource dictionaries) ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) )] // Version information for an assembly consists of the following four values: @@ -51,4 +51,4 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Microsoft.OpenApi/Any/AnyType.cs b/src/Microsoft.OpenApi/Any/AnyType.cs index 12bbec6d4..d0addd808 100644 --- a/src/Microsoft.OpenApi/Any/AnyType.cs +++ b/src/Microsoft.OpenApi/Any/AnyType.cs @@ -28,4 +28,4 @@ public enum AnyType /// Object } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/IOpenApiAny.cs b/src/Microsoft.OpenApi/Any/IOpenApiAny.cs index c8e5f2dea..26c5f4d87 100644 --- a/src/Microsoft.OpenApi/Any/IOpenApiAny.cs +++ b/src/Microsoft.OpenApi/Any/IOpenApiAny.cs @@ -15,4 +15,4 @@ public interface IOpenApiAny : IOpenApiElement, IOpenApiExtension /// AnyType AnyType { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/IOpenApiPrimitive.cs b/src/Microsoft.OpenApi/Any/IOpenApiPrimitive.cs index 79dda83bd..0e286d1a4 100644 --- a/src/Microsoft.OpenApi/Any/IOpenApiPrimitive.cs +++ b/src/Microsoft.OpenApi/Any/IOpenApiPrimitive.cs @@ -74,4 +74,4 @@ public interface IOpenApiPrimitive : IOpenApiAny /// PrimitiveType PrimitiveType { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiArray.cs b/src/Microsoft.OpenApi/Any/OpenApiArray.cs index 73c7a721e..5ef0087f2 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiArray.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiArray.cs @@ -20,7 +20,8 @@ public class OpenApiArray : List, IOpenApiAny /// Write out contents of OpenApiArray to passed writer /// /// Instance of JSON or YAML writer. - public void Write(IOpenApiWriter writer) + /// Version of the OpenAPI specification that that will be output. + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { writer.WriteStartArray(); @@ -33,4 +34,4 @@ public void Write(IOpenApiWriter writer) } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiBinary.cs b/src/Microsoft.OpenApi/Any/OpenApiBinary.cs index d08493912..da1bedad8 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiBinary.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiBinary.cs @@ -22,4 +22,4 @@ public OpenApiBinary(byte[] value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Binary; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiBoolean.cs b/src/Microsoft.OpenApi/Any/OpenApiBoolean.cs index a476bfe38..f531e0135 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiBoolean.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiBoolean.cs @@ -22,4 +22,4 @@ public OpenApiBoolean(bool value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Boolean; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiByte.cs b/src/Microsoft.OpenApi/Any/OpenApiByte.cs index 4847d9bff..5e91b888e 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiByte.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiByte.cs @@ -6,12 +6,20 @@ namespace Microsoft.OpenApi.Any /// /// Open API Byte /// - public class OpenApiByte : OpenApiPrimitive + public class OpenApiByte : OpenApiPrimitive { /// /// Initializes the class. /// public OpenApiByte(byte value) + : this(new byte[] { value }) + { + } + + /// + /// Initializes the class. + /// + public OpenApiByte(byte[] value) : base(value) { } @@ -21,4 +29,4 @@ public OpenApiByte(byte value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Byte; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiDate.cs b/src/Microsoft.OpenApi/Any/OpenApiDate.cs index deec825c9..c285799b6 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiDate.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiDate.cs @@ -23,4 +23,4 @@ public OpenApiDate(DateTime value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Date; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiDateTime.cs b/src/Microsoft.OpenApi/Any/OpenApiDateTime.cs index 4f885fedd..81b647288 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiDateTime.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiDateTime.cs @@ -23,4 +23,4 @@ public OpenApiDateTime(DateTimeOffset value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.DateTime; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiDouble.cs b/src/Microsoft.OpenApi/Any/OpenApiDouble.cs index a86279fb2..35711a191 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiDouble.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiDouble.cs @@ -21,4 +21,4 @@ public OpenApiDouble(double value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Double; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiFloat.cs b/src/Microsoft.OpenApi/Any/OpenApiFloat.cs index fd0189835..3a64fb04c 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiFloat.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiFloat.cs @@ -21,4 +21,4 @@ public OpenApiFloat(float value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Float; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiInteger.cs b/src/Microsoft.OpenApi/Any/OpenApiInteger.cs index aa7a9284d..a0aa88fe8 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiInteger.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiInteger.cs @@ -21,4 +21,4 @@ public OpenApiInteger(int value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Integer; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiLong.cs b/src/Microsoft.OpenApi/Any/OpenApiLong.cs index cf3c9efa6..30b42fbf3 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiLong.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiLong.cs @@ -21,4 +21,4 @@ public OpenApiLong(long value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Long; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiNull.cs b/src/Microsoft.OpenApi/Any/OpenApiNull.cs index 5ff43acfa..229409d95 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiNull.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiNull.cs @@ -19,9 +19,10 @@ public class OpenApiNull : IOpenApiAny /// Write out null representation /// /// - public void Write(IOpenApiWriter writer) + /// Version of the OpenAPI specification that that will be output. + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { writer.WriteAny(this); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiObject.cs b/src/Microsoft.OpenApi/Any/OpenApiObject.cs index 0f1aee397..cd2f6ee64 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiObject.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiObject.cs @@ -20,7 +20,8 @@ public class OpenApiObject : Dictionary, IOpenApiAny /// Serialize OpenApiObject to writer /// /// - public void Write(IOpenApiWriter writer) + /// Version of the OpenAPI specification that that will be output. + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { writer.WriteStartObject(); @@ -34,4 +35,4 @@ public void Write(IOpenApiWriter writer) } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiPassword.cs b/src/Microsoft.OpenApi/Any/OpenApiPassword.cs index caf771690..aaa56e72b 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiPassword.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiPassword.cs @@ -21,4 +21,4 @@ public OpenApiPassword(string value) /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.Password; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiPrimitive.cs b/src/Microsoft.OpenApi/Any/OpenApiPrimitive.cs index dd6be1b95..624dbd395 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiPrimitive.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiPrimitive.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; +using System.Text; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Properties; using Microsoft.OpenApi.Writers; @@ -41,7 +43,8 @@ public OpenApiPrimitive(T value) /// Write out content of primitive element /// /// - public void Write(IOpenApiWriter writer) + /// + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { switch (this.PrimitiveType) { @@ -72,12 +75,28 @@ public void Write(IOpenApiWriter writer) case PrimitiveType.Byte: var byteValue = (OpenApiByte)(IOpenApiPrimitive)this; - writer.WriteValue(byteValue.Value); + if (byteValue.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteValue(Convert.ToBase64String(byteValue.Value)); + } + break; case PrimitiveType.Binary: var binaryValue = (OpenApiBinary)(IOpenApiPrimitive)this; - writer.WriteValue(binaryValue.Value); + if (binaryValue.Value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteValue(Encoding.UTF8.GetString(binaryValue.Value)); + } + break; case PrimitiveType.Boolean: @@ -109,4 +128,4 @@ public void Write(IOpenApiWriter writer) } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Any/OpenApiString.cs b/src/Microsoft.OpenApi/Any/OpenApiString.cs index ebd31449e..e1036cfca 100644 --- a/src/Microsoft.OpenApi/Any/OpenApiString.cs +++ b/src/Microsoft.OpenApi/Any/OpenApiString.cs @@ -8,18 +8,30 @@ namespace Microsoft.OpenApi.Any /// public class OpenApiString : OpenApiPrimitive { + private bool isExplicit; + /// /// Initializes the class. /// /// - public OpenApiString(string value) + /// Used to indicate if a string is quoted. + public OpenApiString(string value, bool isExplicit = false) : base(value) { + this.isExplicit = isExplicit; } /// /// The primitive class this object represents. /// public override PrimitiveType PrimitiveType { get; } = PrimitiveType.String; + + /// + /// True if string was specified explicitly by the means of double quotes, single quotes, or literal or folded style. + /// + public bool IsExplicit() + { + return this.isExplicit; + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Attributes/DisplayAttribute.cs b/src/Microsoft.OpenApi/Attributes/DisplayAttribute.cs index 0d30d313a..920593bbd 100644 --- a/src/Microsoft.OpenApi/Attributes/DisplayAttribute.cs +++ b/src/Microsoft.OpenApi/Attributes/DisplayAttribute.cs @@ -30,4 +30,4 @@ public DisplayAttribute(string name) /// public string Name { get; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Error.cs b/src/Microsoft.OpenApi/Error.cs index 4d9af0810..d0c41780d 100644 --- a/src/Microsoft.OpenApi/Error.cs +++ b/src/Microsoft.OpenApi/Error.cs @@ -95,4 +95,4 @@ internal static NotSupportedException NotSupported(string messageFormat, params return new NotSupportedException(Format(messageFormat, messageArgs)); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Exceptions/OpenApiException.cs b/src/Microsoft.OpenApi/Exceptions/OpenApiException.cs index 78c2ff6a8..410e0dd78 100644 --- a/src/Microsoft.OpenApi/Exceptions/OpenApiException.cs +++ b/src/Microsoft.OpenApi/Exceptions/OpenApiException.cs @@ -43,4 +43,4 @@ public OpenApiException(string message, Exception innerException) /// public string Pointer { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Exceptions/OpenApiWriterException.cs b/src/Microsoft.OpenApi/Exceptions/OpenApiWriterException.cs index fca2538ef..494608b1f 100644 --- a/src/Microsoft.OpenApi/Exceptions/OpenApiWriterException.cs +++ b/src/Microsoft.OpenApi/Exceptions/OpenApiWriterException.cs @@ -38,4 +38,4 @@ public OpenApiWriterException(string message, Exception innerException) { } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Expressions/BodyExpression.cs b/src/Microsoft.OpenApi/Expressions/BodyExpression.cs index 5fc6e25dd..cd5c86bd9 100644 --- a/src/Microsoft.OpenApi/Expressions/BodyExpression.cs +++ b/src/Microsoft.OpenApi/Expressions/BodyExpression.cs @@ -66,4 +66,4 @@ public string Fragment } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs b/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs new file mode 100644 index 000000000..7d5e32c7d --- /dev/null +++ b/src/Microsoft.OpenApi/Expressions/CompositeExpression.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Microsoft.OpenApi.Expressions +{ + /// + /// String literal with embedded expressions + /// + public class CompositeExpression : RuntimeExpression + { + private readonly string template; + private Regex expressionPattern = new Regex(@"{(?\$[^}]*)"); + + /// + /// Expressions embedded into string literal + /// + public List ContainedExpressions = new List(); + + /// + /// Create a composite expression from a string literal with an embedded expression + /// + /// + public CompositeExpression(string expression) + { + template = expression; + + // Extract subexpressions and convert to RuntimeExpressions + var matches = expressionPattern.Matches(expression); + + foreach (var item in matches.Cast()) + { + var value = item.Groups["exp"].Captures.Cast().First().Value; + ContainedExpressions.Add(RuntimeExpression.Build(value)); + } + } + + /// + /// Return original string literal with embedded expression + /// + public override string Expression => template; + } +} diff --git a/src/Microsoft.OpenApi/Expressions/HeaderExpression.cs b/src/Microsoft.OpenApi/Expressions/HeaderExpression.cs index 2165ca230..f6642cb08 100644 --- a/src/Microsoft.OpenApi/Expressions/HeaderExpression.cs +++ b/src/Microsoft.OpenApi/Expressions/HeaderExpression.cs @@ -48,4 +48,4 @@ public string Token } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Expressions/MethodExpression.cs b/src/Microsoft.OpenApi/Expressions/MethodExpression.cs index 7258d5b50..865d9c7ff 100644 --- a/src/Microsoft.OpenApi/Expressions/MethodExpression.cs +++ b/src/Microsoft.OpenApi/Expressions/MethodExpression.cs @@ -18,15 +18,10 @@ public sealed class MethodExpression : RuntimeExpression /// public override string Expression { get; } = Method; - /// - /// Gets the singleton. - /// - public static MethodExpression Instance = new MethodExpression(); - /// /// Private constructor. /// - private MethodExpression() + public MethodExpression() { } } diff --git a/src/Microsoft.OpenApi/Expressions/RuntimeExpression.cs b/src/Microsoft.OpenApi/Expressions/RuntimeExpression.cs index a28b95fc5..965572dfd 100644 --- a/src/Microsoft.OpenApi/Expressions/RuntimeExpression.cs +++ b/src/Microsoft.OpenApi/Expressions/RuntimeExpression.cs @@ -36,25 +36,25 @@ public static RuntimeExpression Build(string expression) if (!expression.StartsWith(Prefix)) { - throw new OpenApiException(string.Format(SRResource.RuntimeExpressionMustBeginWithDollar, expression)); + return new CompositeExpression(expression); } // $url if (expression == UrlExpression.Url) { - return UrlExpression.Instance; + return new UrlExpression(); } // $method if (expression == MethodExpression.Method) { - return MethodExpression.Instance; + return new MethodExpression(); } // $statusCode if (expression == StatusCodeExpression.StatusCode) { - return StatusCodeExpression.Instance; + return new StatusCodeExpression(); } // $request. diff --git a/src/Microsoft.OpenApi/Expressions/StatusCodeExpression.cs b/src/Microsoft.OpenApi/Expressions/StatusCodeExpression.cs index af71787d0..88a384781 100644 --- a/src/Microsoft.OpenApi/Expressions/StatusCodeExpression.cs +++ b/src/Microsoft.OpenApi/Expressions/StatusCodeExpression.cs @@ -18,15 +18,10 @@ public sealed class StatusCodeExpression : RuntimeExpression /// public override string Expression { get; } = StatusCode; - /// - /// Gets the singleton. - /// - public static StatusCodeExpression Instance = new StatusCodeExpression(); - /// /// Private constructor. /// - private StatusCodeExpression() + public StatusCodeExpression() { } } diff --git a/src/Microsoft.OpenApi/Expressions/UrlExpression.cs b/src/Microsoft.OpenApi/Expressions/UrlExpression.cs index 5fd186e81..4dc10bb77 100644 --- a/src/Microsoft.OpenApi/Expressions/UrlExpression.cs +++ b/src/Microsoft.OpenApi/Expressions/UrlExpression.cs @@ -18,16 +18,11 @@ public sealed class UrlExpression : RuntimeExpression /// public override string Expression { get; } = Url; - /// - /// Gets the singleton. - /// - public static UrlExpression Instance = new UrlExpression(); - /// /// Private constructor. /// - private UrlExpression() + public UrlExpression() { } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Extensions/EnumExtensions.cs b/src/Microsoft.OpenApi/Extensions/EnumExtensions.cs index f146e0ee7..d7c778c19 100644 --- a/src/Microsoft.OpenApi/Extensions/EnumExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/EnumExtensions.cs @@ -43,4 +43,4 @@ public static string GetDisplayName(this Enum enumValue) return attribute == null ? enumValue.ToString() : attribute.Name; } } -} \ No newline at end of file +} 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/OpenApiElementExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs index f7e138e2e..81893a3b2 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiElementExtensions.cs @@ -28,4 +28,4 @@ public static IEnumerable Validate(this IOpenApiElement element, V return validator.Errors; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiExtensibleExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiExtensibleExtensions.cs index 6f39041a2..aee0d44a5 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiExtensibleExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiExtensibleExtensions.cs @@ -21,7 +21,7 @@ public static class OpenApiExtensibleExtensions /// The extensible Open API element. /// The extension name. /// The extension value. - public static void AddExtension(this T element, string name, IOpenApiAny any) + public static void AddExtension(this T element, string name, IOpenApiExtension any) where T : IOpenApiExtensible { if (element == null) @@ -41,6 +41,5 @@ public static void AddExtension(this T element, string name, IOpenApiAny any) element.Extensions[name] = any ?? throw Error.ArgumentNull(nameof(any)); } - } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs index a072753a4..e14c98340 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Globalization; using System.IO; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; @@ -49,11 +50,13 @@ public static void SerializeAsYaml(this T element, Stream stream, OpenApiSpec /// The given stream. /// The Open API specification version. /// The output format (JSON or YAML). + /// Provide configuration settings for controlling writing output public static void Serialize( this T element, Stream stream, OpenApiSpecVersion specVersion, - OpenApiFormat format) + OpenApiFormat format, + OpenApiWriterSettings settings = null) where T : IOpenApiSerializable { if (stream == null) @@ -62,13 +65,14 @@ public static void Serialize( } IOpenApiWriter writer; + var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture); switch (format) { case OpenApiFormat.Json: - writer = new OpenApiJsonWriter(new StreamWriter(stream)); + writer = new OpenApiJsonWriter(streamWriter,settings); break; case OpenApiFormat.Yaml: - writer = new OpenApiYamlWriter(new StreamWriter(stream)); + writer = new OpenApiYamlWriter(streamWriter, settings); break; default: throw new OpenApiException(string.Format(SRResource.OpenApiFormatNotSupported, format)); @@ -83,7 +87,8 @@ public static void Serialize( /// the /// The Open API element. /// The output writer. - /// The Open API specification version. + /// Version of the specification the output should conform to + public static void Serialize(this T element, IOpenApiWriter writer, OpenApiSpecVersion specVersion) where T : IOpenApiSerializable { @@ -114,6 +119,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. /// @@ -172,4 +178,4 @@ public static string Serialize( } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Extensions/StringExtensions.cs b/src/Microsoft.OpenApi/Extensions/StringExtensions.cs index 1f8fa9f16..980abda86 100644 --- a/src/Microsoft.OpenApi/Extensions/StringExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/StringExtensions.cs @@ -38,4 +38,4 @@ public static T GetEnumFromDisplayName(this string displayName) return default(T); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiElement.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiElement.cs index 59279eb3c..45f7bad11 100644 --- a/src/Microsoft.OpenApi/Interfaces/IOpenApiElement.cs +++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiElement.cs @@ -9,4 +9,4 @@ namespace Microsoft.OpenApi.Interfaces public interface IOpenApiElement { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiExtensible.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiExtensible.cs index 1760c14e3..7abd1bfdd 100644 --- a/src/Microsoft.OpenApi/Interfaces/IOpenApiExtensible.cs +++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiExtensible.cs @@ -16,4 +16,4 @@ public interface IOpenApiExtensible : IOpenApiElement /// IDictionary Extensions { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiExtension.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiExtension.cs index e9e92fb5d..a9ea04a39 100644 --- a/src/Microsoft.OpenApi/Interfaces/IOpenApiExtension.cs +++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiExtension.cs @@ -14,6 +14,7 @@ public interface IOpenApiExtension /// Write out contents of custom extension /// /// - void Write(IOpenApiWriter writer); + /// Version of the OpenAPI specification that that will be output. + void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion); } } diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiReferenceable.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiReferenceable.cs index cf39f5004..eb47c64bc 100644 --- a/src/Microsoft.OpenApi/Interfaces/IOpenApiReferenceable.cs +++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiReferenceable.cs @@ -15,7 +15,7 @@ public interface IOpenApiReferenceable : IOpenApiSerializable /// /// Indicates if object is populated with data or is just a reference to the data /// - bool UnresolvedReference { get; set;} + bool UnresolvedReference { get; set; } /// /// Reference object. @@ -32,4 +32,4 @@ public interface IOpenApiReferenceable : IOpenApiSerializable /// void SerializeAsV2WithoutReference(IOpenApiWriter writer); } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Interfaces/IOpenApiSerializable.cs b/src/Microsoft.OpenApi/Interfaces/IOpenApiSerializable.cs index d33ec2088..582bd49cd 100644 --- a/src/Microsoft.OpenApi/Interfaces/IOpenApiSerializable.cs +++ b/src/Microsoft.OpenApi/Interfaces/IOpenApiSerializable.cs @@ -22,4 +22,4 @@ public interface IOpenApiSerializable : IOpenApiElement /// The writer. void SerializeAsV2(IOpenApiWriter writer); } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/JsonPointer.cs b/src/Microsoft.OpenApi/JsonPointer.cs index 6ef6b95ef..07e1305d6 100644 --- a/src/Microsoft.OpenApi/JsonPointer.cs +++ b/src/Microsoft.OpenApi/JsonPointer.cs @@ -66,4 +66,4 @@ public override string ToString() return "/" + string.Join("/", Tokens); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj index ccdc2bf18..53a009587 100644 --- a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj +++ b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj @@ -10,7 +10,7 @@ Microsoft Microsoft.OpenApi Microsoft.OpenApi - 1.1.0-preview.1 + 1.2.0-preview.3 .NET models with JSON and YAML writers for OpenAPI specification © Microsoft Corporation. All rights reserved. OpenAPI .NET diff --git a/src/Microsoft.OpenApi/Models/OpenApiCallback.cs b/src/Microsoft.OpenApi/Models/OpenApiCallback.cs index 221881798..4f685d2de 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiCallback.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiCallback.cs @@ -23,7 +23,7 @@ public class OpenApiCallback : IOpenApiSerializable, IOpenApiReferenceable, IOpe /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference pointer. @@ -70,12 +70,12 @@ 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; } - + SerializeAsV3WithoutReference(writer); } @@ -94,7 +94,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) } // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -116,4 +116,4 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) // Callback object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiComponents.cs b/src/Microsoft.OpenApi/Models/OpenApiComponents.cs index 5bc87c1dd..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; @@ -76,6 +78,28 @@ 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 + // 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; + } + writer.WriteStartObject(); // Serialize each referenceable object as full object without reference if the reference in the object points to itself. @@ -244,7 +268,7 @@ public void SerializeAsV3(IOpenApiWriter writer) }); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -257,4 +281,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // Components object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index 526ed25e1..11e6d8f70 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -624,4 +624,4 @@ public static class OpenApiConstants #endregion } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiContact.cs b/src/Microsoft.OpenApi/Models/OpenApiContact.cs index 52f56dc29..848560ead 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiContact.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiContact.cs @@ -40,7 +40,7 @@ public class OpenApiContact : IOpenApiSerializable, IOpenApiExtensible /// public void SerializeAsV3(IOpenApiWriter writer) { - WriteInternal(writer); + WriteInternal(writer, OpenApiSpecVersion.OpenApi3_0); } /// @@ -48,10 +48,10 @@ public void SerializeAsV3(IOpenApiWriter writer) /// public void SerializeAsV2(IOpenApiWriter writer) { - WriteInternal(writer); + WriteInternal(writer, OpenApiSpecVersion.OpenApi2_0); } - private void WriteInternal(IOpenApiWriter writer) + private void WriteInternal(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { if (writer == null) { @@ -70,9 +70,9 @@ private void WriteInternal(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Email, Email); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, specVersion); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs b/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs index cee77aadb..a0739dc25 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDiscriminator.cs @@ -51,4 +51,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // Discriminator object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 2d898850d..f97360b06 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -4,6 +4,8 @@ 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; @@ -34,7 +36,7 @@ public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible /// /// REQUIRED. The available paths and operations for the API. /// - public OpenApiPaths Paths { get; set; } = new OpenApiPaths(); + public OpenApiPaths Paths { get; set; } /// /// An element to hold various schemas for the specification. @@ -102,7 +104,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, e) => e.SerializeAsV3(w)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -131,27 +133,53 @@ 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. + // 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; - // definitions - writer.WriteOptionalMap( - OpenApiConstants.Definitions, - Components?.Schemas, - (w, key, component) => + 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); + + foreach (var schema in openApiSchemas.Values.ToList()) { - component.SerializeAsV2(w); + FindSchemaReferences.ResolveSchemas(Components, openApiSchemas); } - }); + writer.WriteOptionalMap( + OpenApiConstants.Definitions, + openApiSchemas, + (w, key, component) => + { + component.SerializeAsV2WithoutReference(w); + }); + } + } + 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) => + { + 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, @@ -219,7 +247,7 @@ public void SerializeAsV2(IOpenApiWriter writer) writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, e) => e.SerializeAsV2(w)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } @@ -237,15 +265,34 @@ private static void WriteHostInfoV2(IOpenApiWriter writer, IList // Divide the URL in the Url property into host and basePath required in OpenAPI V2 // The Url property cannotcontain path templating to be valid for V2 serialization. - var firstServerUrl = new Uri(firstServer.Url); + var firstServerUrl = new Uri(firstServer.Url, UriKind.RelativeOrAbsolute); // host - writer.WriteProperty( - OpenApiConstants.Host, - firstServerUrl.GetComponents(UriComponents.Host | UriComponents.Port, UriFormat.SafeUnescaped)); - - // basePath - writer.WriteProperty(OpenApiConstants.BasePath, firstServerUrl.AbsolutePath); + if (firstServerUrl.IsAbsoluteUri) + { + writer.WriteProperty( + OpenApiConstants.Host, + firstServerUrl.GetComponents(UriComponents.Host | UriComponents.Port, UriFormat.SafeUnescaped)); + + // basePath + if (firstServerUrl.AbsolutePath != "/") + { + writer.WriteProperty(OpenApiConstants.BasePath, firstServerUrl.AbsolutePath); + } + } else + { + var relativeUrl = firstServerUrl.OriginalString; + if (relativeUrl.StartsWith("//")) + { + var pathPosition = relativeUrl.IndexOf('/', 3); + writer.WriteProperty(OpenApiConstants.Host, relativeUrl.Substring(0, pathPosition)); + relativeUrl = relativeUrl.Substring(pathPosition); + } + if (!String.IsNullOrEmpty(relativeUrl) && relativeUrl != "/") + { + writer.WriteProperty(OpenApiConstants.BasePath, relativeUrl); + } + } // Consider all schemes of the URLs in the server list that have the same // host, port, and base path as the first server. @@ -262,7 +309,7 @@ private static void WriteHostInfoV2(IOpenApiWriter writer, IList UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.SafeUnescaped, StringComparison.OrdinalIgnoreCase) == - 0) + 0 && u.IsAbsoluteUri) .Select(u => u.Scheme) .Distinct() .ToList(); @@ -275,11 +322,12 @@ private static void WriteHostInfoV2(IOpenApiWriter writer, IList /// Walk the OpenApiDocument and resolve unresolved references /// /// Indicates if external references should be resolved. Document needs to reference a workspace for this to be possible. - public void ResolveReferences(bool useExternal = false) + public IEnumerable ResolveReferences(bool useExternal = false) { var resolver = new OpenApiReferenceResolver(this, useExternal); var walker = new OpenApiWalker(resolver); walker.Walk(this); + return resolver.Errors; } /// @@ -295,7 +343,7 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference, bool u if (reference.IsExternal && !useExternal) { // Should not attempt to resolve external references against a single document. - throw new ArgumentException(Properties.SRResource.RemoteReferenceNotSupported); + throw new ArgumentException(Properties.SRResource.RemoteReferenceNotSupported); } if (!reference.Type.HasValue) @@ -318,6 +366,11 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference, bool u return null; } + if (this.Components == null) + { + throw new OpenApiException(string.Format(Properties.SRResource.InvalidReferenceId, reference.Id)); + } + try { switch (reference.Type) @@ -352,10 +405,54 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference, bool u default: throw new OpenApiException(Properties.SRResource.InvalidReferenceType); } - } catch(KeyNotFoundException) + } + catch (KeyNotFoundException) + { + throw new OpenApiException(string.Format(Properties.SRResource.InvalidReferenceId, reference.Id)); + } + } + } + + internal class FindSchemaReferences : OpenApiVisitorBase + { + private Dictionary Schemas; + + public static void ResolveSchemas(OpenApiComponents components, Dictionary schemas ) + { + var visitor = new FindSchemaReferences(); + visitor.Schemas = schemas; + var walker = new OpenApiWalker(visitor); + walker.Walk(components); + } + + public override void Visit(IOpenApiReferenceable referenceable) + { + switch (referenceable) { - throw new OpenApiException(string.Format(Properties.SRResource.InvalidReferenceId,reference.Id)); + 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); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiEncoding.cs b/src/Microsoft.OpenApi/Models/OpenApiEncoding.cs index ccb36ef77..0d02c384e 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiEncoding.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiEncoding.cs @@ -81,7 +81,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.AllowReserved, AllowReserved, false); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -94,4 +94,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // nothing here } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiError.cs b/src/Microsoft.OpenApi/Models/OpenApiError.cs index f58001050..98bf7ec82 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiError.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiError.cs @@ -8,7 +8,7 @@ namespace Microsoft.OpenApi.Models /// /// Error related to the Open API Document. /// - public abstract class OpenApiError + public class OpenApiError { /// /// Initializes the class using the message and pointer from the given exception. @@ -41,7 +41,7 @@ public OpenApiError(string pointer, string message) /// public override string ToString() { - return Message + (!string.IsNullOrEmpty(Pointer) ? " at " + Pointer : ""); + return Message + (!string.IsNullOrEmpty(Pointer) ? " [" + Pointer + "]" : ""); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiExample.cs b/src/Microsoft.OpenApi/Models/OpenApiExample.cs index ae6cfa000..d9bc08e30 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; @@ -93,7 +93,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.ExternalValue, ExternalValue); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -118,4 +118,4 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) // in Response object, so it will be serialized as a part of the Response object. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs b/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs index 643c4bb14..0a43255fb 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiExtensibleDictionary.cs @@ -39,7 +39,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteRequiredObject(item.Key, item.Value, (w, p) => p.SerializeAsV3(w)); } - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -61,9 +61,9 @@ public void SerializeAsV2(IOpenApiWriter writer) writer.WriteRequiredObject(item.Key, item.Value, (w, p) => p.SerializeAsV2(w)); } - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiExternalDocs.cs b/src/Microsoft.OpenApi/Models/OpenApiExternalDocs.cs index 58e283fbf..a47fb9906 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiExternalDocs.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiExternalDocs.cs @@ -34,7 +34,7 @@ public class OpenApiExternalDocs : IOpenApiSerializable, IOpenApiExtensible /// public void SerializeAsV3(IOpenApiWriter writer) { - WriteInternal(writer); + WriteInternal(writer, OpenApiSpecVersion.OpenApi3_0); } /// @@ -42,10 +42,10 @@ public void SerializeAsV3(IOpenApiWriter writer) /// public void SerializeAsV2(IOpenApiWriter writer) { - WriteInternal(writer); + WriteInternal(writer, OpenApiSpecVersion.OpenApi2_0); } - private void WriteInternal(IOpenApiWriter writer) + private void WriteInternal(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { if (writer == null) { @@ -61,9 +61,9 @@ private void WriteInternal(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Url, Url?.OriginalString); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, specVersion); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs index c04135efa..eb6736183 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiHeader.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiHeader.cs @@ -18,7 +18,7 @@ public class OpenApiHeader : IOpenApiSerializable, IOpenApiReferenceable, IOpenA /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference pointer. @@ -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; @@ -146,7 +146,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteOptionalMap(OpenApiConstants.Content, Content, (w, c) => c.SerializeAsV3(w)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -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; @@ -205,9 +205,9 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, s) => w.WriteAny(s)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiInfo.cs b/src/Microsoft.OpenApi/Models/OpenApiInfo.cs index 126b60d46..17364ba3b 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiInfo.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiInfo.cs @@ -80,7 +80,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Version, Version); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -116,9 +116,9 @@ public void SerializeAsV2(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Version, Version); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiLicense.cs b/src/Microsoft.OpenApi/Models/OpenApiLicense.cs index 5c147f42e..0de7540c8 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiLicense.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiLicense.cs @@ -34,7 +34,7 @@ public class OpenApiLicense : IOpenApiSerializable, IOpenApiExtensible /// public void SerializeAsV3(IOpenApiWriter writer) { - WriteInternal(writer); + WriteInternal(writer, OpenApiSpecVersion.OpenApi3_0); } /// @@ -42,10 +42,10 @@ public void SerializeAsV3(IOpenApiWriter writer) /// public void SerializeAsV2(IOpenApiWriter writer) { - WriteInternal(writer); + WriteInternal(writer, OpenApiSpecVersion.OpenApi2_0); } - private void WriteInternal(IOpenApiWriter writer) + private void WriteInternal(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { if (writer == null) { @@ -61,9 +61,9 @@ private void WriteInternal(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Url, Url?.OriginalString); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, specVersion); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiLink.cs b/src/Microsoft.OpenApi/Models/OpenApiLink.cs index 4bd7ef4bf..fb7396db2 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiLink.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiLink.cs @@ -54,7 +54,7 @@ public class OpenApiLink : IOpenApiSerializable, IOpenApiReferenceable, IOpenApi /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference pointer. @@ -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; @@ -124,4 +124,4 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) // Link object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs b/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs index 6f6e05106..414f46b30 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiMediaType.cs @@ -68,7 +68,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteOptionalMap(OpenApiConstants.Encoding, Encoding, (w, e) => e.SerializeAsV3(w)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -81,4 +81,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // Media type does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiOAuthFlow.cs b/src/Microsoft.OpenApi/Models/OpenApiOAuthFlow.cs index 0c4072330..e478944aa 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOAuthFlow.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOAuthFlow.cs @@ -66,7 +66,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteRequiredMap(OpenApiConstants.Scopes, Scopes, (w, s) => w.WriteValue(s)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -79,4 +79,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // OAuthFlow object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiOAuthFlows.cs b/src/Microsoft.OpenApi/Models/OpenApiOAuthFlows.cs index 97cff9b34..fa2db10db 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOAuthFlows.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOAuthFlows.cs @@ -69,7 +69,7 @@ public void SerializeAsV3(IOpenApiWriter writer) (w, o) => o.SerializeAsV3(w)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -82,4 +82,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // OAuthFlows object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs index 5d3a16e33..50c6fa584 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiOperation.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiOperation.cs @@ -161,7 +161,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteOptionalCollection(OpenApiConstants.Servers, Servers, (w, s) => s.SerializeAsV3(w)); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -247,7 +247,7 @@ public void SerializeAsV2(IOpenApiWriter writer) // V2 spec actually allows the body to have custom name. // Our library does not support this at the moment. Name = "body", - Schema = content?.Schema, + Schema = content?.Schema ?? new OpenApiSchema(), Required = RequestBody.Required }; @@ -309,9 +309,9 @@ public void SerializeAsV2(IOpenApiWriter writer) writer.WriteOptionalCollection(OpenApiConstants.Security, Security, (w, s) => s.SerializeAsV2(w)); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs index 31d4f13e8..97edce131 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiParameter.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiParameter.cs @@ -17,7 +17,7 @@ public class OpenApiParameter : IOpenApiSerializable, IOpenApiReferenceable, IOp /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference object. @@ -101,7 +101,7 @@ public class OpenApiParameter : IOpenApiSerializable, IOpenApiReferenceable, IOp /// Furthermore, if referencing a schema which contains an example, /// the examples value SHALL override the example provided by the schema. /// - public IDictionary Examples { get; set; } = new Dictionary(); + public IDictionary Examples { get; set; } = new Dictionary(); /// /// Example of the media type. The example SHOULD match the specified schema and encoding properties @@ -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; @@ -159,7 +159,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Name, Name); // in - writer.WriteProperty(OpenApiConstants.In, In.GetDisplayName()); + writer.WriteProperty(OpenApiConstants.In, In?.GetDisplayName()); // description writer.WriteProperty(OpenApiConstants.Description, Description); @@ -177,7 +177,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Style, Style?.GetDisplayName()); // explode - writer.WriteProperty(OpenApiConstants.Explode, Explode, false); + writer.WriteProperty(OpenApiConstants.Explode, Explode, Style.HasValue && Style.Value == ParameterStyle.Form); // allowReserved writer.WriteProperty(OpenApiConstants.AllowReserved, AllowReserved, false); @@ -195,7 +195,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteOptionalMap(OpenApiConstants.Content, Content, (w, c) => c.SerializeAsV3(w)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -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; @@ -237,7 +237,7 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) } else { - writer.WriteProperty(OpenApiConstants.In, In.GetDisplayName()); + writer.WriteProperty(OpenApiConstants.In, In?.GetDisplayName()); } // name @@ -252,6 +252,8 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) // deprecated writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false); + var extensionsClone = new Dictionary(Extensions); + // schema if (this is OpenApiBodyParameter) { @@ -259,7 +261,7 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) } // In V2 parameter's type can't be a reference to a custom object schema or can't be of type object // So in that case map the type as string. - else + else if (Schema?.UnresolvedReference == true || Schema?.Type == "object") { writer.WriteProperty(OpenApiConstants.Type, "string"); @@ -283,12 +285,25 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) // uniqueItems // enum // multipleOf - Schema?.WriteAsItemsProperties(writer); + if (Schema != null) + { + Schema.WriteAsItemsProperties(writer); + + if (Schema.Extensions != null) + { + foreach (var key in Schema.Extensions.Keys) + { + // The extension will already have been serialized as part of the call to WriteAsItemsProperties above, + // so remove it from the cloned collection so we don't write it again. + extensionsClone.Remove(key); + } + } + } // allowEmptyValue writer.WriteProperty(OpenApiConstants.AllowEmptyValue, AllowEmptyValue, false); - if (this.In == ParameterLocation.Query ) + if (this.In == ParameterLocation.Query) { if (this.Style == ParameterStyle.Form && this.Explode == true) { @@ -307,7 +322,7 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(extensionsClone, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } @@ -326,4 +341,4 @@ internal class OpenApiBodyParameter : OpenApiParameter internal class OpenApiFormDataParameter : OpenApiParameter { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiPathItem.cs b/src/Microsoft.OpenApi/Models/OpenApiPathItem.cs index 4da8363d0..3143e94cb 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiPathItem.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiPathItem.cs @@ -90,7 +90,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteOptionalCollection(OpenApiConstants.Parameters, Parameters, (w, p) => p.SerializeAsV3(w)); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -131,9 +131,9 @@ public void SerializeAsV2(IOpenApiWriter writer) Description); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs index c998a101a..72d0576d3 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiPaths.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiPaths.cs @@ -9,4 +9,4 @@ namespace Microsoft.OpenApi.Models public class OpenApiPaths : OpenApiExtensibleDictionary { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiReference.cs b/src/Microsoft.OpenApi/Models/OpenApiReference.cs index 6e925efd5..2c8f738ca 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiReference.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiReference.cs @@ -210,4 +210,4 @@ private string GetReferenceTypeNameAsV2(ReferenceType type) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs index 80f9a0a7f..d6308efcf 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs @@ -16,7 +16,7 @@ public class OpenApiRequestBody : IOpenApiSerializable, IOpenApiReferenceable, I /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference object. @@ -81,7 +81,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Required, Required, false); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -102,4 +102,4 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) // RequestBody object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs index 1b9064345..bc2e4e525 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponse.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponse.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Writers; @@ -45,7 +44,7 @@ public class OpenApiResponse : IOpenApiSerializable, IOpenApiReferenceable, IOpe /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference pointer. @@ -62,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; @@ -79,7 +78,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteStartObject(); // description - writer.WriteProperty(OpenApiConstants.Description, Description); + writer.WriteRequiredProperty(OpenApiConstants.Description, Description); // headers writer.WriteOptionalMap(OpenApiConstants.Headers, Headers, (w, h) => h.SerializeAsV3(w)); @@ -91,7 +90,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteOptionalMap(OpenApiConstants.Links, Links, (w, l) => l.SerializeAsV3(w)); // extension - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -106,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; @@ -123,7 +122,10 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) writer.WriteStartObject(); // description - writer.WriteProperty(OpenApiConstants.Description, Description); + writer.WriteRequiredProperty(OpenApiConstants.Description, Description); + + var extensionsClone = new Dictionary(Extensions); + if (Content != null) { var mediatype = Content.FirstOrDefault(); @@ -136,14 +138,31 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) (w, s) => s.SerializeAsV2(w)); // examples - if (mediatype.Value.Example != null) + if (Content.Values.Any(m => m.Example != null)) { writer.WritePropertyName(OpenApiConstants.Examples); writer.WriteStartObject(); - writer.WritePropertyName(mediatype.Key); - writer.WriteAny(mediatype.Value.Example); + + foreach (var mediaTypePair in Content) + { + if (mediaTypePair.Value.Example != null) + { + writer.WritePropertyName(mediaTypePair.Key); + writer.WriteAny(mediaTypePair.Value.Example); + } + } + writer.WriteEndObject(); } + + writer.WriteExtensions(mediatype.Value.Extensions, OpenApiSpecVersion.OpenApi2_0); + + foreach (var key in mediatype.Value.Extensions.Keys) + { + // The extension will already have been serialized as part of the call above, + // so remove it from the cloned collection so we don't write it again. + extensionsClone.Remove(key); + } } } @@ -151,9 +170,9 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) writer.WriteOptionalMap(OpenApiConstants.Headers, Headers, (w, h) => h.SerializeAsV2(w)); // extension - writer.WriteExtensions(Extensions); + writer.WriteExtensions(extensionsClone, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs index 0244d99cf..818bf4ced 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiResponses.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiResponses.cs @@ -9,4 +9,4 @@ namespace Microsoft.OpenApi.Models public class OpenApiResponses : OpenApiExtensibleDictionary { } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiSchema.cs b/src/Microsoft.OpenApi/Models/OpenApiSchema.cs index 45a7874df..10760994f 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)); } + 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(); + } } /// @@ -383,7 +401,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Deprecated, Deprecated, false); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -426,10 +444,23 @@ internal void SerializeAsV2( if (Reference != null) { - Reference.SerializeAsV2(writer); - return; + var settings = writer.GetSettings(); + if (settings.ReferenceInline != ReferenceInlineSetting.InlineLocalReferences) + { + Reference.SerializeAsV2(writer); + return; + } + + // If Loop is detected then just Serialize as a reference. + if (!settings.LoopDetector.PushLoop(this)) + { + settings.LoopDetector.SaveLoop(this); + Reference.SerializeAsV2(writer); + return; + } } + if (parentRequiredProperties == null) { parentRequiredProperties = new HashSet(); @@ -516,7 +547,7 @@ internal void WriteAsItemsProperties(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.MultipleOf, MultipleOf); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); } internal void WriteAsSchemaProperties( @@ -626,7 +657,7 @@ internal void WriteAsSchemaProperties( writer.WriteOptionalObject(OpenApiConstants.Example, Example, (w, e) => w.WriteAny(e)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityRequirement.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityRequirement.cs index a94797b6f..d2564daf2 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSecurityRequirement.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityRequirement.cs @@ -146,4 +146,4 @@ public int GetHashCode(OpenApiSecurityScheme obj) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs index 79d87d976..7694c5fd4 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiSecurityScheme.cs @@ -66,7 +66,7 @@ public class OpenApiSecurityScheme : IOpenApiSerializable, IOpenApiReferenceable /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference object. @@ -134,7 +134,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) } // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -212,7 +212,7 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Description, Description); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } @@ -259,4 +259,4 @@ private static void WriteOAuthFlowForV2(IOpenApiWriter writer, string flowValue, writer.WriteOptionalMap(OpenApiConstants.Scopes, flow.Scopes, (w, s) => w.WriteValue(s)); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiServer.cs b/src/Microsoft.OpenApi/Models/OpenApiServer.cs index a29d17f06..ea988ec13 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiServer.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiServer.cs @@ -58,7 +58,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteOptionalMap(OpenApiConstants.Variables, Variables, (w, v) => v.SerializeAsV3(w)); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -71,4 +71,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // Server object does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs b/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs index 695f09965..8ae39a04c 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiServerVariable.cs @@ -56,7 +56,7 @@ public void SerializeAsV3(IOpenApiWriter writer) writer.WriteOptionalCollection(OpenApiConstants.Enum, Enum, (w, s) => w.WriteValue(s)); // specification extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -69,4 +69,4 @@ public void SerializeAsV2(IOpenApiWriter writer) // ServerVariable does not exist in V2. } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiTag.cs b/src/Microsoft.OpenApi/Models/OpenApiTag.cs index dc3462191..4d743a13a 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiTag.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiTag.cs @@ -36,7 +36,7 @@ public class OpenApiTag : IOpenApiSerializable, IOpenApiReferenceable, IOpenApiE /// /// Indicates if object is populated with data or is just a reference to the data /// - public bool UnresolvedReference { get; set;} + public bool UnresolvedReference { get; set; } /// /// Reference. @@ -79,7 +79,7 @@ public void SerializeAsV3WithoutReference(IOpenApiWriter writer) writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, e) => e.SerializeAsV3(w)); // extensions. - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0); writer.WriteEndObject(); } @@ -120,9 +120,9 @@ public void SerializeAsV2WithoutReference(IOpenApiWriter writer) writer.WriteOptionalObject(OpenApiConstants.ExternalDocs, ExternalDocs, (w, e) => e.SerializeAsV2(w)); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi2_0); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OpenApiXml.cs b/src/Microsoft.OpenApi/Models/OpenApiXml.cs index 3d4ae7bf9..59218970f 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiXml.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiXml.cs @@ -51,7 +51,7 @@ public class OpenApiXml : IOpenApiSerializable, IOpenApiExtensible /// public void SerializeAsV3(IOpenApiWriter writer) { - Write(writer); + Write(writer, OpenApiSpecVersion.OpenApi3_0); } /// @@ -59,10 +59,10 @@ public void SerializeAsV3(IOpenApiWriter writer) /// public void SerializeAsV2(IOpenApiWriter writer) { - Write(writer); + Write(writer, OpenApiSpecVersion.OpenApi2_0); } - private void Write(IOpenApiWriter writer) + private void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { if (writer == null) { @@ -87,9 +87,9 @@ private void Write(IOpenApiWriter writer) writer.WriteProperty(OpenApiConstants.Wrapped, Wrapped, false); // extensions - writer.WriteExtensions(Extensions); + writer.WriteExtensions(Extensions, specVersion); writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/OperationType.cs b/src/Microsoft.OpenApi/Models/OperationType.cs index 8c0d9038e..a85933d9b 100644 --- a/src/Microsoft.OpenApi/Models/OperationType.cs +++ b/src/Microsoft.OpenApi/Models/OperationType.cs @@ -50,4 +50,4 @@ public enum OperationType /// [Display("trace")] Trace } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/ParameterLocation.cs b/src/Microsoft.OpenApi/Models/ParameterLocation.cs index 4b15a335c..a729f314e 100644 --- a/src/Microsoft.OpenApi/Models/ParameterLocation.cs +++ b/src/Microsoft.OpenApi/Models/ParameterLocation.cs @@ -31,4 +31,4 @@ public enum ParameterLocation /// [Display("cookie")] Cookie } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/ParameterStyle.cs b/src/Microsoft.OpenApi/Models/ParameterStyle.cs index 5837d3704..a1df27962 100644 --- a/src/Microsoft.OpenApi/Models/ParameterStyle.cs +++ b/src/Microsoft.OpenApi/Models/ParameterStyle.cs @@ -45,4 +45,4 @@ public enum ParameterStyle /// [Display("deepObject")] DeepObject } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/ReferenceType.cs b/src/Microsoft.OpenApi/Models/ReferenceType.cs index 18205dcb4..6ac0c9ed2 100644 --- a/src/Microsoft.OpenApi/Models/ReferenceType.cs +++ b/src/Microsoft.OpenApi/Models/ReferenceType.cs @@ -60,4 +60,4 @@ public enum ReferenceType /// [Display("tags")] Tag } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs b/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs index 1bff9c146..12a525b4f 100644 --- a/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs +++ b/src/Microsoft.OpenApi/Models/RuntimeExpressionAnyWrapper.cs @@ -68,4 +68,4 @@ public void WriteValue(IOpenApiWriter writer) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs b/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs index a52b6a70a..d75524bd6 100644 --- a/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs +++ b/src/Microsoft.OpenApi/Models/SecuritySchemeType.cs @@ -30,4 +30,4 @@ public enum SecuritySchemeType /// [Display("openIdConnect")] OpenIdConnect } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/OpenApiFormat.cs b/src/Microsoft.OpenApi/OpenApiFormat.cs index 140d71bb1..8f4c55b68 100644 --- a/src/Microsoft.OpenApi/OpenApiFormat.cs +++ b/src/Microsoft.OpenApi/OpenApiFormat.cs @@ -18,4 +18,4 @@ public enum OpenApiFormat /// Yaml } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/OpenApiSerializerSettings.cs b/src/Microsoft.OpenApi/OpenApiSerializerSettings.cs deleted file mode 100644 index 1357af105..000000000 --- a/src/Microsoft.OpenApi/OpenApiSerializerSettings.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.OpenApi -{ - /// - /// Configuration settings for Open API writers. - /// - public sealed class OpenApiSerializerSettings - { - /// - /// Open Api specification version - /// - public OpenApiSpecVersion SpecVersion { get; set; } = OpenApiSpecVersion.OpenApi3_0; - - /// - /// Open Api document format. - /// - public OpenApiFormat Format { get; set; } = OpenApiFormat.Json; - } -} \ No newline at end of file diff --git a/src/Microsoft.OpenApi/OpenApiSpecVersion.cs b/src/Microsoft.OpenApi/OpenApiSpecVersion.cs index 687f6d774..20e49af80 100644 --- a/src/Microsoft.OpenApi/OpenApiSpecVersion.cs +++ b/src/Microsoft.OpenApi/OpenApiSpecVersion.cs @@ -18,4 +18,4 @@ public enum OpenApiSpecVersion /// OpenApi3_0 } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Properties/AssemblyInfo.cs b/src/Microsoft.OpenApi/Properties/AssemblyInfo.cs index 24ddadb3e..159c979dd 100644 --- a/src/Microsoft.OpenApi/Properties/AssemblyInfo.cs +++ b/src/Microsoft.OpenApi/Properties/AssemblyInfo.cs @@ -8,4 +8,4 @@ "Microsoft.OpenApi.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] [assembly: InternalsVisibleTo( - "Microsoft.OpenApi.Readers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] \ No newline at end of file + "Microsoft.OpenApi.Readers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 03e493776..47f9493b9 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -339,6 +339,16 @@ internal static string Validation_RuleAddTwice { } } + /// + /// Looks up a localized string similar to Schema {0} must contain property specified in the discriminator {1} in the required field list.. + /// + internal static string Validation_SchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator { + get { + return ResourceManager.GetString("Validation_SchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminat" + + "or", resourceCulture); + } + } + /// /// Looks up a localized string similar to The string '{0}' MUST be in the format of an email address.. /// diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index 6d9ffc112..48f910a95 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -210,6 +210,9 @@ The same rule cannot be in the same rule set twice. + + Schema {0} must contain property specified in the discriminator {1} in the required field list. + The string '{0}' MUST be in the format of an email address. diff --git a/src/Microsoft.OpenApi/Services/ComparisonContext.cs b/src/Microsoft.OpenApi/Services/ComparisonContext.cs new file mode 100644 index 000000000..42acf5bda --- /dev/null +++ b/src/Microsoft.OpenApi/Services/ComparisonContext.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// A class encapsulating the comparision context. + /// + public class ComparisonContext + { + private readonly IList _openApiDifferences = new List(); + private readonly Stack _path = new Stack(); + internal readonly OpenApiDocument SourceDocument; + internal readonly Stack SourceSchemaLoop = new Stack(); + internal readonly OpenApiDocument TargetDocument; + internal readonly Stack TargetSchemaLoop = new Stack(); + internal OpenApiComparerFactory OpenApiComparerFactory; + + /// + /// Creates instance of . + /// + public ComparisonContext( + OpenApiComparerFactory openApiComparerFactory, + OpenApiDocument sourceDocument, + OpenApiDocument targetDocument) + { + OpenApiComparerFactory = openApiComparerFactory; + SourceDocument = sourceDocument; + TargetDocument = targetDocument; + } + + /// + /// Gets the list of open api differences. + /// + public IEnumerable OpenApiDifferences => _openApiDifferences; + + /// + /// Pointer to the source of difference in the document. + /// + public string PathString => "#/" + string.Join("/", _path.Reverse()); + + /// + /// Adds an open api difference. + /// + /// The open api difference to add. + public void AddOpenApiDifference(OpenApiDifference openApiDifference) + { + if (openApiDifference == null) + { + throw Error.ArgumentNull(nameof(openApiDifference)); + } + + _openApiDifferences.Add(openApiDifference); + } + + /// + /// Allow Rule to indicate difference occured at a deeper context level. + /// + /// Identifier for the context. + public void Enter(string segment) + { + _path.Push(segment); + } + + /// + /// Exit from path context level. Enter and Exit calls should be matched. + /// + public void Exit() + { + _path.Pop(); + } + + /// + /// Gets the comparer instance for the requested type. + /// + /// Type of requested comparer. + /// Comparer instance to use when comparing requested type. + internal OpenApiComparerBase GetComparer() + { + return OpenApiComparerFactory.GetComparer(); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/LoopDetector.cs b/src/Microsoft.OpenApi/Services/LoopDetector.cs new file mode 100644 index 000000000..249cab51d --- /dev/null +++ b/src/Microsoft.OpenApi/Services/LoopDetector.cs @@ -0,0 +1,71 @@ +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 + /// + /// 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 + /// + 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 + /// + internal void ClearLoop() + { + _loopStacks[typeof(T)].Clear(); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiAnyComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiAnyComparer.cs new file mode 100644 index 000000000..886f4313d --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiAnyComparer.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.IO; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Writers; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiAnyComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + IOpenApiAny source, + IOpenApiAny target, + ComparisonContext comparisonContext) + { + if (source == null && target == null) + { + return; + } + + if (source == null || target == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = source, + TargetValue = target, + OpenApiComparedElementType = typeof(IOpenApiAny), + Pointer = comparisonContext.PathString + }); + + return; + } + + var sourceStringWriter = new StringWriter(); + var sourceWriter = new OpenApiJsonWriter(sourceStringWriter); + + source.Write(sourceWriter, OpenApiSpecVersion.OpenApi3_0); + var sourceValue = sourceStringWriter.GetStringBuilder().ToString(); + + var targetStringWriter = new StringWriter(); + var targetWriter = new OpenApiJsonWriter(targetStringWriter); + + target.Write(targetWriter, OpenApiSpecVersion.OpenApi3_0); + var targetValue = targetStringWriter.GetStringBuilder().ToString(); + + if (string.Compare(sourceValue, targetValue, StringComparison.InvariantCulture) != 0) + { + comparisonContext.AddOpenApiDifference(new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = source, + TargetValue = target, + Pointer = comparisonContext.PathString + }); + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiComparer.cs index a8faa625a..cfe73e65e 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiComparer.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiComparer.cs @@ -14,10 +14,23 @@ public static class OpenApiComparer /// /// Compares two s and returns a list of differences. /// - public static List Compare(OpenApiDocument source, OpenApiDocument target) + public static IEnumerable Compare(OpenApiDocument source, OpenApiDocument target) { - var diffs = new List(); - return diffs; + if (source == null) + { + throw Error.ArgumentNull(nameof(source)); + } + + if (target == null) + { + throw Error.ArgumentNull(nameof(target)); + } + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), source, target); + + new OpenApiDocumentComparer().Compare(source, target, comparisonContext); + + return comparisonContext.OpenApiDifferences; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiComparerBase.cs b/src/Microsoft.OpenApi/Services/OpenApiComparerBase.cs new file mode 100644 index 000000000..53762688b --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiComparerBase.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing parts of class. + /// + /// Type of class to compare. + public abstract class OpenApiComparerBase + { + /// + /// Validates a fragment of . + /// + /// The source fragment. + /// The target fragment. + /// Context under which to compare fragment. + public abstract void Compare(T sourceFragment, T targetFragment, ComparisonContext comparisonContext); + + /// + /// Compares two string object. + /// + /// The source string. + /// The target string. + /// The context under which to compare the objects. + internal void Compare(string source, string target, ComparisonContext comparisonContext) + { + if (string.IsNullOrWhiteSpace(source) && string.IsNullOrWhiteSpace(target)) + { + return; + } + + if (string.Compare(source, target, StringComparison.CurrentCultureIgnoreCase) != 0) + { + comparisonContext.AddOpenApiDifference(new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = source, + TargetValue = target, + Pointer = comparisonContext.PathString + }); + } + } + + /// + /// Compares two Uri object. + /// + /// The source. + /// The target. + /// The context under which to compare the objects. + internal void Compare(Uri source, Uri target, ComparisonContext comparisonContext) + { + if (source == null && target == null) + { + return; + } + + if (source != target) + { + comparisonContext.AddOpenApiDifference(new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + SourceValue = source, + TargetValue = target, + Pointer = comparisonContext.PathString + }); + } + } + + /// + /// Compares two boolean object. + /// + /// The source. + /// The target. + /// The context under which to compare the objects. + internal void Compare(bool? source, bool? target, ComparisonContext comparisonContext) + { + if (source == null && target == null) + { + return; + } + + if (source != target) + { + comparisonContext.AddOpenApiDifference(new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + SourceValue = source, + TargetValue = target, + Pointer = comparisonContext.PathString + }); + } + } + + /// + /// Compares two decimal object. + /// + /// The source. + /// The target. + /// The context under which to compare the objects. + internal void Compare(decimal? source, decimal? target, ComparisonContext comparisonContext) + { + if (source == null && target == null) + { + return; + } + + if (source != target) + { + comparisonContext.AddOpenApiDifference(new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(decimal?), + SourceValue = source, + TargetValue = target, + Pointer = comparisonContext.PathString + }); + } + } + + /// + /// Compares Enum. + /// + /// The source. + /// The target. + /// The context under which to compare the objects. + internal void Compare(Enum source, Enum target, ComparisonContext comparisonContext) + { + if (source == null && target == null) + { + return; + } + + if (source == null || target == null) + { + comparisonContext.AddOpenApiDifference(new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(TEnum), + SourceValue = source, + TargetValue = target, + Pointer = comparisonContext.PathString + }); + + return; + } + + if (!source.Equals(target)) + { + comparisonContext.AddOpenApiDifference(new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(TEnum), + SourceValue = source, + TargetValue = target, + Pointer = comparisonContext.PathString + }); + } + } + + /// + /// Compares where TKey is and TValue is + /// . + /// + /// The source. + /// The target. + /// The context under which to compare the objects. + internal void Compare(IDictionary source, IDictionary target, + ComparisonContext comparisonContext) + { + if (source == null && target == null) + { + return; + } + + if (source == null || target == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = source, + TargetValue = target, + OpenApiComparedElementType = typeof(IDictionary), + Pointer = comparisonContext.PathString + }); + + return; + } + + var newKeysInTarget = target.Keys.Except(source.Keys).ToList(); + + foreach (var newKeyInTarget in newKeysInTarget) + { + WalkAndAddOpenApiDifference( + comparisonContext, + newKeyInTarget, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = target[newKeyInTarget], + OpenApiComparedElementType = typeof(string) + }); + } + + var removedKeysFromSource = source.Keys.Except(target.Keys).ToList(); + + foreach (var removedKeyFromSource in removedKeysFromSource) + { + WalkAndAddOpenApiDifference( + comparisonContext, + removedKeyFromSource, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = source[removedKeyFromSource], + OpenApiComparedElementType = typeof(string) + }); + } + } + + /// + /// Adds a segment to the context path to enable pointing to the current location in the document. + /// + /// The context under which to compare the objects. + /// An identifier for the segment. + /// The open api difference to add. + internal void WalkAndAddOpenApiDifference( + ComparisonContext comparisonContext, + string segment, + OpenApiDifference openApiDifference) + { + comparisonContext.Enter(segment.Replace("~", "~0").Replace("/", "~1")); + openApiDifference.Pointer = comparisonContext.PathString; + comparisonContext.AddOpenApiDifference(openApiDifference); + comparisonContext.Exit(); + } + + /// + /// Adds a segment to the context path to enable pointing to the current location in the document. + /// + /// The context under which to compare the objects. + /// An identifier for the segment. + /// An action that compares objects within the context. + protected virtual void WalkAndCompare( + ComparisonContext comparisonContext, + string segment, + Action compare) + { + comparisonContext.Enter(segment.Replace("~", "~0").Replace("/", "~1")); + compare(); + comparisonContext.Exit(); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiComparerFactory.cs b/src/Microsoft.OpenApi/Services/OpenApiComparerFactory.cs new file mode 100644 index 000000000..5ff7b6481 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiComparerFactory.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for registering specific comparer instances and encapsulates default comparers. + /// + public class OpenApiComparerFactory + { + private static readonly Dictionary TypeToDefaultComparerMap = new Dictionary + { + {typeof(OpenApiPaths), new OpenApiPathsComparer()}, + {typeof(OpenApiPathItem), new OpenApiPathItemComparer()}, + {typeof(OpenApiOperation), new OpenApiOperationComparer()}, + {typeof(IDictionary), new OpenApiOperationsComparer()}, + {typeof(IList), new OpenApiParametersComparer()}, + {typeof(OpenApiParameter), new OpenApiParameterComparer()}, + {typeof(OpenApiSchema), new OpenApiSchemaComparer()}, + {typeof(OpenApiMediaType), new OpenApiMediaTypeComparer()}, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + { + typeof(IDictionary), + new OpenApiDictionaryComparer() + }, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + { + typeof(IDictionary), + new OpenApiDictionaryComparer() + }, + {typeof(OpenApiHeader), new OpenApiHeaderComparer()}, + {typeof(OpenApiRequestBody), new OpenApiRequestBodyComparer()}, + {typeof(OpenApiResponse), new OpenApiResponseComparer()}, + {typeof(OpenApiComponents), new OpenApiComponentsComparer()}, + {typeof(OpenApiEncoding), new OpenApiEncodingComparer()}, + {typeof(IList), new OpenApiServersComparer()}, + {typeof(OpenApiServer), new OpenApiServerComparer()}, + {typeof(OpenApiServerVariable), new OpenApiServerVariableComparer()}, + {typeof(OpenApiOAuthFlow), new OpenApiOAuthFlowComparer()}, + {typeof(OpenApiOAuthFlows), new OpenApiOAuthFlowsComparer()}, + {typeof(OpenApiSecurityRequirement), new OpenApiSecurityRequirementComparer()}, + {typeof(OpenApiInfo), new OpenApiInfoComparer()}, + {typeof(OpenApiContact), new OpenApiContactComparer()}, + {typeof(OpenApiLicense), new OpenApiLicenseComparer()}, + {typeof(IList), new OpenApiOrderedListComparer()}, + {typeof(IList), new OpenApiOrderedListComparer()}, + {typeof(OpenApiExternalDocs), new OpenApiExternalDocsComparer()}, + {typeof(OpenApiTag), new OpenApiTagComparer()}, + {typeof(OpenApiSecurityScheme), new OpenApiSecuritySchemeComparer()}, + {typeof(OpenApiExample), new OpenApiExampleComparer()}, + {typeof(IDictionary), new OpenApiDictionaryComparer()}, + {typeof(IOpenApiAny), new OpenApiAnyComparer()} + }; + + private readonly Dictionary _typeToComparerMap = new Dictionary(); + + /// + /// Adds a comparer instance to this registry. + /// + /// Type of the comparer instance. + /// Instance of to register. + protected void AddComparer(OpenApiComparerBase comparer) + { + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + _typeToComparerMap.Add(typeof(T), comparer); + } + + /// + /// Gets a registered comparer instance for the requested type. + /// + /// Type of the comparer. + /// The comparer instance corresponding to the type requested. + internal OpenApiComparerBase GetComparer() + { + var requestedComparerType = typeof(T); + + if (_typeToComparerMap.TryGetValue(requestedComparerType, out object comparerInstance)) + { + return (OpenApiComparerBase)comparerInstance; + } + + if (!TypeToDefaultComparerMap.TryGetValue(requestedComparerType, out comparerInstance)) + { + throw Error.NotSupported( + $"No comparer is registered for type {requestedComparerType.Name}."); + } + + return (OpenApiComparerBase)comparerInstance; + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiComponentsComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiComponentsComparer.cs new file mode 100644 index 000000000..2dd397ef7 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiComponentsComparer.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiComponentsComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiComponents sourceComponents, + OpenApiComponents targetComponents, + ComparisonContext comparisonContext) + { + if (sourceComponents == null && targetComponents == null) + { + return; + } + + if (sourceComponents == null || targetComponents == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceComponents, + TargetValue = targetComponents, + OpenApiComparedElementType = typeof(OpenApiComponents), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Parameters, + () => comparisonContext + .GetComparer>() + .Compare(sourceComponents.Parameters, targetComponents.Parameters, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.RequestBodies, + () => comparisonContext + .GetComparer>() + .Compare(sourceComponents.RequestBodies, targetComponents.RequestBodies, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Responses, + () => comparisonContext + .GetComparer>() + .Compare(sourceComponents.Responses, targetComponents.Responses, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Schemas, + () => comparisonContext + .GetComparer>() + .Compare(sourceComponents.Schemas, targetComponents.Schemas, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Headers, + () => comparisonContext + .GetComparer>() + .Compare(sourceComponents.Headers, targetComponents.Headers, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.SecuritySchemes, + () => comparisonContext + .GetComparer>() + .Compare(sourceComponents.SecuritySchemes, targetComponents.SecuritySchemes, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Examples, + () => comparisonContext + .GetComparer>() + .Compare(sourceComponents.Examples, targetComponents.Examples, comparisonContext)); + + // To Do compare Links + // To Do compare Callbacks + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiContactComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiContactComparer.cs new file mode 100644 index 000000000..1c0626ade --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiContactComparer.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiContactComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiContact sourceContact, + OpenApiContact targetContact, + ComparisonContext comparisonContext) + { + if (sourceContact == null && targetContact == null) + { + return; + } + + if (sourceContact == null || targetContact == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceContact, + TargetValue = targetContact, + OpenApiComparedElementType = typeof(OpenApiContact), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Name, + () => Compare(sourceContact.Name, targetContact.Name, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Email, + () => Compare(sourceContact.Email, targetContact.Email, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Url, + () => Compare(sourceContact.Url, targetContact.Url, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiDictionaryComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiDictionaryComparer.cs new file mode 100644 index 000000000..89e9c7431 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiDictionaryComparer.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Interfaces; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing where TKey is + /// and TValue is . + /// + public class OpenApiDictionaryComparer : OpenApiComparerBase> + where T : IOpenApiSerializable + { + /// + /// Executes comparision against source and target + /// where TKey is and TValue is . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + IDictionary sourceFragment, + IDictionary targetFragment, + ComparisonContext comparisonContext) + { + if (sourceFragment == null && targetFragment == null) + { + return; + } + + if (sourceFragment == null || targetFragment == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceFragment, + TargetValue = targetFragment, + OpenApiComparedElementType = typeof(IDictionary), + Pointer = comparisonContext.PathString + }); + + return; + } + + var newKeysInTarget = targetFragment.Keys.Except(sourceFragment.Keys).ToList(); + + foreach (var newKeyInTarget in newKeysInTarget) + { + WalkAndAddOpenApiDifference( + comparisonContext, + newKeyInTarget, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = targetFragment[newKeyInTarget], + OpenApiComparedElementType = typeof(T) + }); + } + + foreach (var source in sourceFragment) + { + if (targetFragment.Keys.Contains(source.Key)) + { + WalkAndCompare(comparisonContext, source.Key, + () => comparisonContext + .GetComparer() + .Compare(source.Value, targetFragment[source.Key], comparisonContext)); + } + else + { + WalkAndAddOpenApiDifference( + comparisonContext, + source.Key, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = source.Value, + OpenApiComparedElementType = typeof(T) + }); + } + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiDifference.cs b/src/Microsoft.OpenApi/Services/OpenApiDifference.cs index aea6d19f2..bd6262c5b 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiDifference.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiDifference.cs @@ -1,14 +1,39 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using Microsoft.OpenApi.Models; namespace Microsoft.OpenApi.Services { /// - /// Difference point between two + /// Difference point between two . /// public class OpenApiDifference { + /// + /// The type of the element for which difference found. + /// + public Type OpenApiComparedElementType { get; set; } + + /// + /// The open api difference operation. + /// + public OpenApiDifferenceOperation OpenApiDifferenceOperation { get; set; } + + /// + /// Pointer to the location of the difference. + /// + public string Pointer { get; set; } + + /// + /// The source value. + /// + public object SourceValue { get; set; } + + /// + /// The target value. + /// + public object TargetValue { get; set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiDifferenceOperation.cs b/src/Microsoft.OpenApi/Services/OpenApiDifferenceOperation.cs new file mode 100644 index 000000000..896c0a4d3 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiDifferenceOperation.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.OpenApi.Services +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + /// + /// The open api difference operation. + /// + public enum OpenApiDifferenceOperation + { + Add, + Remove, + Update + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiDocumentComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiDocumentComparer.cs new file mode 100644 index 000000000..7ac8786b7 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiDocumentComparer.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiDocumentComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiDocument sourceDocument, + OpenApiDocument targetDocument, + ComparisonContext comparisonContext) + { + WalkAndCompare( + comparisonContext, + OpenApiConstants.Paths, + () => comparisonContext + .GetComparer() + .Compare(sourceDocument.Paths, targetDocument.Paths, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Components, + () => comparisonContext + .GetComparer() + .Compare(sourceDocument.Components, targetDocument.Components, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Components, + () => comparisonContext + .GetComparer>() + .Compare(sourceDocument.Servers, targetDocument.Servers, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Info, + () => comparisonContext + .GetComparer() + .Compare(sourceDocument.Info, targetDocument.Info, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Security, + () => comparisonContext + .GetComparer>() + .Compare(sourceDocument.SecurityRequirements, targetDocument.SecurityRequirements, + comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Tags, + () => comparisonContext + .GetComparer>() + .Compare(sourceDocument.Tags, targetDocument.Tags, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.ExternalDocs, + () => comparisonContext + .GetComparer() + .Compare(sourceDocument.ExternalDocs, targetDocument.ExternalDocs, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiEncodingComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiEncodingComparer.cs new file mode 100644 index 000000000..ed7349299 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiEncodingComparer.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiEncodingComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiEncoding sourceEncoding, + OpenApiEncoding targetEncoding, + ComparisonContext comparisonContext) + { + if (sourceEncoding == null && targetEncoding == null) + { + return; + } + + if (sourceEncoding == null || targetEncoding == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceEncoding, + TargetValue = targetEncoding, + OpenApiComparedElementType = typeof(OpenApiEncoding), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.ContentType, + () => Compare(sourceEncoding.ContentType, targetEncoding.ContentType, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Explode, + () => Compare(sourceEncoding.Explode, targetEncoding.Explode, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.AllowReserved, + () => Compare(sourceEncoding.AllowReserved, targetEncoding.AllowReserved, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Style, + () => Compare(sourceEncoding.Style, targetEncoding.Style, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Headers, + () => comparisonContext + .GetComparer>() + .Compare(sourceEncoding.Headers, targetEncoding.Headers, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiExampleComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiExampleComparer.cs new file mode 100644 index 000000000..73c35668b --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiExampleComparer.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiExampleComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiExample sourceExample, + OpenApiExample targetExample, + ComparisonContext comparisonContext) + { + if (sourceExample == null && targetExample == null) + { + return; + } + + if (sourceExample == null || targetExample == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceExample, + TargetValue = targetExample, + OpenApiComparedElementType = typeof(OpenApiExample), + Pointer = comparisonContext.PathString + }); + + return; + } + + new OpenApiReferenceComparer() + .Compare(sourceExample.Reference, targetExample.Reference, comparisonContext); + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceExample.Description, targetExample.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Summary, + () => Compare(sourceExample.Summary, targetExample.Summary, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.ExternalValue, + () => Compare(sourceExample.ExternalValue, targetExample.ExternalValue, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Value, + () => comparisonContext + .GetComparer() + .Compare(sourceExample.Value, targetExample.Value, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiExternalDocsComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiExternalDocsComparer.cs new file mode 100644 index 000000000..b5eea40e4 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiExternalDocsComparer.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiExternalDocsComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare(OpenApiExternalDocs sourceDocs, OpenApiExternalDocs targetDocs, + ComparisonContext comparisonContext) + { + if (sourceDocs == null && targetDocs == null) + { + return; + } + + if (sourceDocs == null || targetDocs == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceDocs, + TargetValue = targetDocs, + OpenApiComparedElementType = typeof(OpenApiExternalDocs), + Pointer = comparisonContext.PathString + }); + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceDocs.Description, targetDocs.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Url, + () => Compare(sourceDocs.Url, targetDocs.Url, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiHeaderComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiHeaderComparer.cs new file mode 100644 index 000000000..f80fa977c --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiHeaderComparer.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiHeaderComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiHeader sourceHeader, + OpenApiHeader targetHeader, + ComparisonContext comparisonContext) + { + if (sourceHeader == null && targetHeader == null) + { + return; + } + + if (sourceHeader == null || targetHeader == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceHeader, + TargetValue = targetHeader, + OpenApiComparedElementType = typeof(OpenApiHeader), + Pointer = comparisonContext.PathString + }); + + return; + } + + if (sourceHeader.Reference != null + && targetHeader.Reference != null + && sourceHeader.Reference.Id != targetHeader.Reference.Id) + { + WalkAndAddOpenApiDifference( + comparisonContext, + OpenApiConstants.DollarRef, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceHeader.Reference, + TargetValue = targetHeader.Reference, + OpenApiComparedElementType = typeof(OpenApiReference) + }); + + return; + } + + if (sourceHeader.Reference != null) + { + sourceHeader = (OpenApiHeader)comparisonContext.SourceDocument.ResolveReference( + sourceHeader.Reference); + } + + if (targetHeader.Reference != null) + { + targetHeader = (OpenApiHeader)comparisonContext.TargetDocument.ResolveReference( + targetHeader.Reference); + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceHeader.Description, targetHeader.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Required, + () => Compare(sourceHeader.Required, targetHeader.Required, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Deprecated, + () => Compare(sourceHeader.Deprecated, targetHeader.Deprecated, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.AllowEmptyValue, + () => Compare(sourceHeader.AllowEmptyValue, targetHeader.AllowEmptyValue, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Explode, + () => Compare(sourceHeader.Explode, targetHeader.Explode, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.AllowReserved, + () => Compare(sourceHeader.AllowReserved, targetHeader.AllowReserved, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Content, + () => comparisonContext + .GetComparer>() + .Compare(sourceHeader.Content, targetHeader.Content, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Schema, + () => comparisonContext + .GetComparer() + .Compare(sourceHeader.Schema, targetHeader.Schema, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Examples, + () => comparisonContext + .GetComparer>() + .Compare(sourceHeader.Examples, targetHeader.Examples, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Example, + () => comparisonContext + .GetComparer() + .Compare(sourceHeader.Example, targetHeader.Example, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiInfoComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiInfoComparer.cs new file mode 100644 index 000000000..0d391ed82 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiInfoComparer.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiInfoComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiInfo sourceInfo, + OpenApiInfo targetInfo, + ComparisonContext comparisonContext) + { + if (sourceInfo == null && targetInfo == null) + { + return; + } + + if (sourceInfo == null || targetInfo == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceInfo, + TargetValue = targetInfo, + OpenApiComparedElementType = typeof(OpenApiInfo), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Title, + () => Compare(sourceInfo.Title, targetInfo.Title, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceInfo.Description, targetInfo.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.TermsOfService, + () => Compare(sourceInfo.TermsOfService, targetInfo.TermsOfService, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Version, + () => Compare(sourceInfo.Version, targetInfo.Version, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Contact, + () => comparisonContext + .GetComparer() + .Compare(sourceInfo.Contact, targetInfo.Contact, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.License, + () => comparisonContext + .GetComparer() + .Compare(sourceInfo.License, targetInfo.License, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiLicenseComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiLicenseComparer.cs new file mode 100644 index 000000000..ad0b06e17 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiLicenseComparer.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiLicenseComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiLicense sourceLicense, + OpenApiLicense targetLicense, + ComparisonContext comparisonContext) + { + if (sourceLicense == null && targetLicense == null) + { + return; + } + + if (sourceLicense == null || targetLicense == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceLicense, + TargetValue = targetLicense, + OpenApiComparedElementType = typeof(OpenApiLicense), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Name, + () => Compare(sourceLicense.Name, targetLicense.Name, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Url, + () => Compare(sourceLicense.Url, targetLicense.Url, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiMediaTypeComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiMediaTypeComparer.cs new file mode 100644 index 000000000..69767e88e --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiMediaTypeComparer.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiMediaTypeComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiMediaType sourceMediaType, + OpenApiMediaType targetMediaType, + ComparisonContext comparisonContext) + { + if (sourceMediaType == null && targetMediaType == null) + { + return; + } + + if (sourceMediaType == null || targetMediaType == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceMediaType, + TargetValue = targetMediaType, + OpenApiComparedElementType = typeof(OpenApiMediaType), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Schema, + () => comparisonContext + .GetComparer() + .Compare(sourceMediaType.Schema, targetMediaType.Schema, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Encoding, + () => comparisonContext + .GetComparer>() + .Compare(sourceMediaType.Encoding, sourceMediaType.Encoding, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Examples, + () => comparisonContext + .GetComparer>() + .Compare(sourceMediaType.Examples, targetMediaType.Examples, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Example, + () => comparisonContext + .GetComparer() + .Compare(sourceMediaType.Example, targetMediaType.Example, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiOAuthFlowComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiOAuthFlowComparer.cs new file mode 100644 index 000000000..56bcfd47d --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiOAuthFlowComparer.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiOAuthFlowComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare(OpenApiOAuthFlow sourceFlow, OpenApiOAuthFlow targetFlow, + ComparisonContext comparisonContext) + { + if (sourceFlow == null && targetFlow == null) + { + return; + } + + if (sourceFlow == null || targetFlow == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceFlow, + TargetValue = targetFlow, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.AuthorizationUrl, + () => Compare(sourceFlow.AuthorizationUrl, targetFlow.AuthorizationUrl, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.TokenUrl, + () => Compare(sourceFlow.TokenUrl, targetFlow.TokenUrl, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.RefreshUrl, + () => Compare(sourceFlow.RefreshUrl, targetFlow.RefreshUrl, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Scopes, + () => Compare(sourceFlow.Scopes, targetFlow.Scopes, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiOAuthFlowsComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiOAuthFlowsComparer.cs new file mode 100644 index 000000000..8856107d3 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiOAuthFlowsComparer.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiOAuthFlowsComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiOAuthFlows sourceFlows, + OpenApiOAuthFlows targetFlows, + ComparisonContext comparisonContext) + { + if (sourceFlows == null && targetFlows == null) + { + return; + } + + if (sourceFlows == null || targetFlows == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceFlows, + TargetValue = targetFlows, + OpenApiComparedElementType = typeof(OpenApiOAuthFlows), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Implicit, + () => comparisonContext + .GetComparer() + .Compare(sourceFlows.Implicit, targetFlows.Implicit, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Password, + () => comparisonContext + .GetComparer() + .Compare(sourceFlows.Password, targetFlows.Password, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.ClientCredentials, + () => comparisonContext + .GetComparer() + .Compare(sourceFlows.ClientCredentials, targetFlows.ClientCredentials, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.AuthorizationCode, + () => comparisonContext + .GetComparer() + .Compare(sourceFlows.AuthorizationCode, targetFlows.AuthorizationCode, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiOperationComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiOperationComparer.cs new file mode 100644 index 000000000..64e38cff4 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiOperationComparer.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiOperationComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiOperation sourceOperation, + OpenApiOperation targetOperation, + ComparisonContext comparisonContext) + { + if (sourceOperation == null && targetOperation == null) + { + return; + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Summary, + () => Compare(sourceOperation?.Summary, targetOperation?.Summary, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Summary, + () => Compare(sourceOperation?.Description, targetOperation?.Description, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.OperationId, + () => Compare(sourceOperation?.OperationId, targetOperation?.OperationId, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Deprecated, + () => Compare(sourceOperation?.Deprecated, targetOperation?.Deprecated, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Parameters, + () => comparisonContext + .GetComparer>() + .Compare(sourceOperation?.Parameters, targetOperation?.Parameters, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.RequestBody, + () => comparisonContext + .GetComparer() + .Compare(sourceOperation?.RequestBody, targetOperation?.RequestBody, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Responses, + () => comparisonContext + .GetComparer>() + .Compare(sourceOperation?.Responses, targetOperation?.Responses, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Servers, + () => comparisonContext + .GetComparer>() + .Compare(sourceOperation?.Servers, targetOperation?.Servers, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Tags, + () => comparisonContext + .GetComparer>() + .Compare(sourceOperation?.Tags, targetOperation?.Tags, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Security, + () => comparisonContext + .GetComparer>() + .Compare(sourceOperation?.Security, targetOperation?.Security, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.ExternalDocs, + () => comparisonContext + .GetComparer() + .Compare(sourceOperation?.ExternalDocs, targetOperation?.ExternalDocs, comparisonContext)); + + // Compare CallBack + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiOperationsComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiOperationsComparer.cs new file mode 100644 index 000000000..78b8e0aa1 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiOperationsComparer.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of + /// where TKey is and TValue is . + /// + public class OpenApiOperationsComparer : OpenApiComparerBase> + { + /// + /// Executes comparision against source and target + /// where TKey is and TValue is . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + IDictionary sourceOperations, + IDictionary targetOperations, + ComparisonContext comparisonContext) + { + if (sourceOperations == null && targetOperations == null) + { + return; + } + + if (sourceOperations == null || targetOperations == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceOperations, + TargetValue = targetOperations, + OpenApiComparedElementType = typeof(IDictionary), + Pointer = comparisonContext.PathString + }); + + return; + } + + var newOperationKeysInTarget = targetOperations.Keys.Except(sourceOperations.Keys).ToList(); + + foreach (var newOperationKeyInTarget in newOperationKeysInTarget) + { + WalkAndAddOpenApiDifference( + comparisonContext, + newOperationKeyInTarget.GetDisplayName(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = targetOperations[newOperationKeyInTarget], + OpenApiComparedElementType = typeof(OpenApiOperation) + }); + } + + foreach (var sourceOperation in sourceOperations) + { + if (targetOperations.Keys.Contains(sourceOperation.Key)) + { + WalkAndCompare(comparisonContext, sourceOperation.Key.GetDisplayName(), + () => comparisonContext + .GetComparer() + .Compare(sourceOperation.Value, targetOperations[sourceOperation.Key], comparisonContext)); + } + else + { + WalkAndAddOpenApiDifference( + comparisonContext, + sourceOperation.Key.GetDisplayName(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = sourceOperation.Value, + OpenApiComparedElementType = typeof(OpenApiOperation) + }); + } + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiOrderedListComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiOrderedListComparer.cs new file mode 100644 index 000000000..b4ac53a5c --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiOrderedListComparer.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Interfaces; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing where T is . + /// + public class OpenApiOrderedListComparer : OpenApiComparerBase> where T : IOpenApiSerializable + { + /// + /// Executes comparision against based on the order of the list for source and target + /// where T is . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + IList sourceFragment, + IList targetFragment, + ComparisonContext comparisonContext) + { + if (sourceFragment == null && targetFragment == null) + { + return; + } + + if (sourceFragment == null || targetFragment == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceFragment, + TargetValue = sourceFragment, + OpenApiComparedElementType = typeof(IList), + Pointer = comparisonContext.PathString + }); + + return; + } + + for (var i = 0; i < sourceFragment.Count; i++) + { + if (i >= targetFragment.Count) + { + WalkAndAddOpenApiDifference( + comparisonContext, + i.ToString(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = sourceFragment[i], + OpenApiComparedElementType = typeof(T) + }); + } + else + { + WalkAndCompare(comparisonContext, + i.ToString(), + () => comparisonContext + .GetComparer() + .Compare(sourceFragment[i], targetFragment[i], comparisonContext)); + } + } + + if (targetFragment.Count <= sourceFragment.Count) + { + return; + } + + // Loop through remaining elements in target that are not in source. + for (var i = sourceFragment.Count; i < targetFragment.Count; i++) + { + WalkAndAddOpenApiDifference( + comparisonContext, + i.ToString(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = targetFragment[i], + OpenApiComparedElementType = typeof(T) + }); + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiParameterComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiParameterComparer.cs new file mode 100644 index 000000000..f3540bacc --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiParameterComparer.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiParameterComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiParameter sourceParameter, + OpenApiParameter targetParameter, + ComparisonContext comparisonContext) + { + if (sourceParameter == null && targetParameter == null) + { + return; + } + + if (sourceParameter == null || targetParameter == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceParameter, + TargetValue = targetParameter, + OpenApiComparedElementType = typeof(OpenApiParameter), + Pointer = comparisonContext.PathString + }); + + return; + } + + new OpenApiReferenceComparer() + .Compare(sourceParameter.Reference, targetParameter.Reference, comparisonContext); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Content, + () => comparisonContext + .GetComparer>() + .Compare(sourceParameter.Content, targetParameter.Content, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceParameter.Description, targetParameter.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Required, + () => Compare(sourceParameter.Required, targetParameter.Required, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Name, + () => Compare(sourceParameter.Name, targetParameter.Name, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Deprecated, + () => Compare(sourceParameter.Deprecated, targetParameter.Deprecated, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.AllowEmptyValue, + () => Compare(sourceParameter.AllowEmptyValue, targetParameter.AllowEmptyValue, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Explode, + () => Compare(sourceParameter.Explode, targetParameter.Explode, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.AllowReserved, + () => Compare(sourceParameter.AllowReserved, targetParameter.AllowReserved, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Style, + () => Compare(sourceParameter.Style, targetParameter.Style, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.In, + () => Compare(sourceParameter.In, targetParameter.In, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Schema, + () => comparisonContext + .GetComparer() + .Compare(sourceParameter.Schema, targetParameter.Schema, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Examples, + () => comparisonContext + .GetComparer>() + .Compare(sourceParameter.Examples, targetParameter.Examples, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Example, + () => comparisonContext + .GetComparer() + .Compare(sourceParameter.Example, targetParameter.Example, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiParametersComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiParametersComparer.cs new file mode 100644 index 000000000..81fd8905f --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiParametersComparer.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of + /// where T is. + /// + public class OpenApiParametersComparer : OpenApiComparerBase> + { + /// + /// Executes comparision against source and target + /// where T is. + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + IList sourceParameters, + IList targetParameters, + ComparisonContext comparisonContext) + { + if (sourceParameters == null && targetParameters == null) + { + return; + } + + if (sourceParameters == null || targetParameters == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceParameters, + TargetValue = targetParameters, + OpenApiComparedElementType = typeof(IList), + Pointer = comparisonContext.PathString + }); + + return; + } + + var removedParameters = sourceParameters?.Where( + sourceParam => !targetParameters.Any( + targetParam => sourceParam.Name == targetParam.Name && sourceParam.In == targetParam.In)).ToList(); + + for (var i = removedParameters.Count - 1; i >= 0; i--) + { + WalkAndAddOpenApiDifference( + comparisonContext, + sourceParameters.IndexOf(removedParameters[i]).ToString(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = removedParameters[i], + OpenApiComparedElementType = typeof(OpenApiParameter) + }); + } + + var newParametersInTarget = targetParameters?.Where( + targetParam => !sourceParameters.Any( + sourceParam => sourceParam.Name == targetParam.Name && sourceParam.In == targetParam.In)).ToList(); + + foreach (var newParameterInTarget in newParametersInTarget) + { + WalkAndAddOpenApiDifference( + comparisonContext, + targetParameters.IndexOf(newParameterInTarget).ToString(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = newParameterInTarget, + OpenApiComparedElementType = typeof(OpenApiParameter) + }); + } + + foreach (var sourceParameter in sourceParameters) + { + var targetParameter = targetParameters + .FirstOrDefault(param => param.Name == sourceParameter.Name && param.In == sourceParameter.In); + + if (targetParameter == null) + { + continue; + } + + WalkAndCompare( + comparisonContext, + targetParameters.IndexOf(targetParameter).ToString(), + () => comparisonContext + .GetComparer() + .Compare(sourceParameter, targetParameter, comparisonContext)); + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiPathItemComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiPathItemComparer.cs new file mode 100644 index 000000000..8e33dc8be --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiPathItemComparer.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiPathItemComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiPathItem sourcePathItem, + OpenApiPathItem targetPathItem, + ComparisonContext comparisonContext) + { + if (sourcePathItem == null && targetPathItem == null) + { + return; + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Summary, + () => Compare(sourcePathItem?.Summary, targetPathItem?.Description, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Description, + () => Compare(sourcePathItem?.Description, targetPathItem?.Description, comparisonContext)); + + comparisonContext.GetComparer>() + .Compare(sourcePathItem?.Operations, targetPathItem?.Operations, comparisonContext); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Parameters, + () => comparisonContext + .GetComparer>() + .Compare(sourcePathItem?.Parameters, targetPathItem?.Parameters, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Servers, + () => comparisonContext + .GetComparer>() + .Compare(sourcePathItem?.Servers, targetPathItem?.Servers, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiPathsComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiPathsComparer.cs new file mode 100644 index 000000000..83b0f58df --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiPathsComparer.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiPathsComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiPaths sourcePaths, + OpenApiPaths targetPaths, + ComparisonContext comparisonContext) + { + if (sourcePaths == null && targetPaths == null) + { + return; + } + + if (sourcePaths == null || targetPaths == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourcePaths, + TargetValue = targetPaths, + OpenApiComparedElementType = typeof(OpenApiPaths), + Pointer = comparisonContext.PathString + }); + + return; + } + + var newPathKeysInTarget = targetPaths.Keys.Except(sourcePaths?.Keys).ToList(); + + foreach (var newPathKeyInTarget in newPathKeysInTarget) + { + WalkAndAddOpenApiDifference( + comparisonContext, + newPathKeyInTarget, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = targetPaths[newPathKeyInTarget], + OpenApiComparedElementType = typeof(OpenApiPathItem) + }); + } + + foreach (var sourcePathKey in sourcePaths.Keys) + { + if (targetPaths.ContainsKey(sourcePathKey)) + { + WalkAndCompare( + comparisonContext, + sourcePathKey, + () => comparisonContext + .GetComparer() + .Compare(sourcePaths[sourcePathKey], targetPaths[sourcePathKey], comparisonContext)); + } + else + { + WalkAndAddOpenApiDifference( + comparisonContext, + sourcePathKey, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = sourcePaths[sourcePathKey], + OpenApiComparedElementType = typeof(OpenApiPathItem) + }); + } + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiReferenceComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiReferenceComparer.cs new file mode 100644 index 000000000..c3909b986 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiReferenceComparer.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiReferenceComparer : OpenApiComparerBase where T : IOpenApiReferenceable + { + /// + /// Compares object. + /// + /// The source. + /// The target. + /// The context under which to compare the objects. + public override void Compare( + OpenApiReference sourceReference, + OpenApiReference targetReference, + ComparisonContext comparisonContext) + { + if (sourceReference == null && targetReference == null) + { + return; + } + + if (sourceReference == null || targetReference == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceReference, + TargetValue = targetReference, + OpenApiComparedElementType = typeof(OpenApiReference), + Pointer = comparisonContext.PathString + }); + + return; + } + + if (sourceReference.Id != targetReference.Id || sourceReference.Type != targetReference.Type) + { + WalkAndAddOpenApiDifference( + comparisonContext, + OpenApiConstants.DollarRef, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceReference, + TargetValue = targetReference, + OpenApiComparedElementType = typeof(OpenApiReference) + }); + + return; + } + + var source = (T)comparisonContext.SourceDocument.ResolveReference( + sourceReference); + + var target = (T)comparisonContext.TargetDocument.ResolveReference( + targetReference); + + comparisonContext + .GetComparer() + .Compare(source, target, comparisonContext); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs b/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs index 03c2703ce..7928c0b43 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiReferenceResolver.cs @@ -20,7 +20,7 @@ internal class OpenApiReferenceResolver : OpenApiVisitorBase private bool _resolveRemoteReferences; private List _errors = new List(); - public OpenApiReferenceResolver(OpenApiDocument currentDocument, bool resolveRemoteReferences = true) + public OpenApiReferenceResolver(OpenApiDocument currentDocument, bool resolveRemoteReferences = true) { _currentDocument = currentDocument; _resolveRemoteReferences = resolveRemoteReferences; @@ -66,7 +66,7 @@ public override void Visit(OpenApiOperation operation) { ResolveObject(operation.RequestBody, r => operation.RequestBody = r); ResolveList(operation.Parameters); - + if (operation.Tags != null) { ResolveTags(operation.Tags); @@ -108,11 +108,14 @@ public override void Visit(OpenApiSecurityRequirement securityRequirement) { ResolveObject(scheme, (resolvedScheme) => { - // If scheme was unresolved - // copy Scopes and remove old unresolved scheme - var scopes = securityRequirement[scheme]; - securityRequirement.Remove(scheme); - securityRequirement.Add(resolvedScheme, scopes); + if (resolvedScheme != null) + { + // If scheme was unresolved + // copy Scopes and remove old unresolved scheme + var scopes = securityRequirement[scheme]; + securityRequirement.Remove(scheme); + securityRequirement.Add(resolvedScheme, scopes); + } }); } } @@ -125,6 +128,16 @@ public override void Visit(IList parameters) ResolveList(parameters); } + /// + /// Resolve all references used in a parameter + /// + public override void Visit(OpenApiParameter parameter) + { + ResolveObject(parameter.Schema, r => parameter.Schema = r); + ResolveMap(parameter.Examples); + } + + /// /// Resolve all references to links /// diff --git a/src/Microsoft.OpenApi/Services/OpenApiRequestBodyComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiRequestBodyComparer.cs new file mode 100644 index 000000000..c49dd3697 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiRequestBodyComparer.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiRequestBodyComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiRequestBody sourceRequestBody, + OpenApiRequestBody targetRequestBody, + ComparisonContext comparisonContext) + { + if (sourceRequestBody == null && targetRequestBody == null) + { + return; + } + + if (sourceRequestBody == null || targetRequestBody == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceRequestBody, + TargetValue = targetRequestBody, + OpenApiComparedElementType = typeof(OpenApiRequestBody), + Pointer = comparisonContext.PathString + }); + + return; + } + + new OpenApiReferenceComparer() + .Compare(sourceRequestBody.Reference, targetRequestBody.Reference, comparisonContext); + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceRequestBody.Description, targetRequestBody.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Required, + () => Compare(sourceRequestBody.Required, targetRequestBody.Required, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Content, + () => comparisonContext + .GetComparer>() + .Compare(sourceRequestBody.Content, targetRequestBody.Content, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiResponseComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiResponseComparer.cs new file mode 100644 index 000000000..1b5af7170 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiResponseComparer.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiResponseComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiResponse sourceResponse, + OpenApiResponse targetResponse, + ComparisonContext comparisonContext) + { + if (sourceResponse == null && targetResponse == null) + { + return; + } + + if (sourceResponse == null || targetResponse == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceResponse, + TargetValue = targetResponse, + OpenApiComparedElementType = typeof(OpenApiResponse), + Pointer = comparisonContext.PathString + }); + + return; + } + + if (sourceResponse.Reference != null + && targetResponse.Reference != null + && sourceResponse.Reference.Id != targetResponse.Reference.Id) + { + WalkAndAddOpenApiDifference( + comparisonContext, + OpenApiConstants.DollarRef, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceResponse.Reference, + TargetValue = targetResponse.Reference, + OpenApiComparedElementType = typeof(OpenApiReference) + }); + + return; + } + + if (sourceResponse.Reference != null) + { + sourceResponse = (OpenApiResponse)comparisonContext.SourceDocument.ResolveReference( + sourceResponse.Reference); + } + + if (targetResponse.Reference != null) + { + targetResponse = (OpenApiResponse)comparisonContext.TargetDocument.ResolveReference( + targetResponse.Reference); + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceResponse.Description, targetResponse.Description, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Content, + () => comparisonContext + .GetComparer>() + .Compare(sourceResponse.Content, targetResponse.Content, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Headers, + () => comparisonContext + .GetComparer>() + .Compare(sourceResponse.Headers, targetResponse.Headers, comparisonContext)); + + // To Do Compare Link + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiSchemaComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiSchemaComparer.cs new file mode 100644 index 000000000..f9c1163ce --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiSchemaComparer.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiSchemaComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiSchema sourceSchema, + OpenApiSchema targetSchema, + ComparisonContext comparisonContext) + { + if (sourceSchema == null && targetSchema == null) + { + return; + } + + if (sourceSchema == null || targetSchema == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceSchema, + TargetValue = targetSchema, + OpenApiComparedElementType = typeof(OpenApiSchema), + Pointer = comparisonContext.PathString + }); + + return; + } + + if (comparisonContext.SourceSchemaLoop.Contains(sourceSchema) + || comparisonContext.SourceSchemaLoop.Contains(targetSchema)) + { + return; // Loop detected, this schema has already been walked. + } + + comparisonContext.SourceSchemaLoop.Push(sourceSchema); + comparisonContext.TargetSchemaLoop.Push(targetSchema); + + if (sourceSchema.Reference != null + && targetSchema.Reference != null + && sourceSchema.Reference.Id != targetSchema.Reference.Id) + { + WalkAndAddOpenApiDifference( + comparisonContext, + OpenApiConstants.DollarRef, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceSchema.Reference?.Id, + TargetValue = targetSchema.Reference?.Id, + OpenApiComparedElementType = typeof(string) + }); + + return; + } + + if (sourceSchema.Reference != null) + { + sourceSchema = (OpenApiSchema)comparisonContext.SourceDocument.ResolveReference( + sourceSchema.Reference); + } + + if (targetSchema.Reference != null) + { + targetSchema = (OpenApiSchema)comparisonContext.TargetDocument.ResolveReference( + targetSchema.Reference); + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Title, + () => Compare(sourceSchema.Title, targetSchema.Title, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Maximum, + () => Compare(sourceSchema.Maximum, targetSchema.Maximum, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.MultipleOf, + () => Compare(sourceSchema.MultipleOf, targetSchema.MultipleOf, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.ExclusiveMaximum, + () => Compare(sourceSchema.ExclusiveMaximum, targetSchema.ExclusiveMaximum, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Minimum, + () => Compare(sourceSchema.Minimum, targetSchema.Minimum, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.ExclusiveMinimum, + () => Compare(sourceSchema.ExclusiveMinimum, targetSchema.ExclusiveMinimum, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.MaxLength, + () => Compare(sourceSchema.MaxLength, targetSchema.MaxLength, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.MinLength, + () => Compare(sourceSchema.MinLength, targetSchema.MinLength, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.MaxItems, + () => Compare(sourceSchema.MaxItems, targetSchema.MaxItems, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.MinItems, + () => Compare(sourceSchema.MinItems, targetSchema.MinItems, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Format, + () => Compare(sourceSchema.Format, targetSchema.Format, comparisonContext)); + + if (sourceSchema.Type != targetSchema.Type) + { + WalkAndAddOpenApiDifference( + comparisonContext, + OpenApiConstants.Type, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceSchema.Type, + TargetValue = targetSchema.Type, + OpenApiComparedElementType = typeof(string) + }); + + return; + } + + if (sourceSchema.Items != null && targetSchema.Items != null) + { + WalkAndCompare( + comparisonContext, + OpenApiConstants.Items, + () => comparisonContext + .GetComparer() + .Compare(sourceSchema.Items, targetSchema.Items, comparisonContext)); + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Properties, + () => comparisonContext + .GetComparer>() + .Compare(sourceSchema.Properties, + targetSchema.Properties, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.ExternalDocs, + () => comparisonContext + .GetComparer() + .Compare(sourceSchema.ExternalDocs, targetSchema.ExternalDocs, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Example, + () => comparisonContext + .GetComparer() + .Compare(sourceSchema.Example, targetSchema.Example, comparisonContext)); + + // To Do Compare schema.AllOf + // To Do Compare schema.AnyOf + + comparisonContext.SourceSchemaLoop.Pop(); + comparisonContext.TargetSchemaLoop.Pop(); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiSecurityRequirementComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiSecurityRequirementComparer.cs new file mode 100644 index 000000000..0753aecb7 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiSecurityRequirementComparer.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiSecurityRequirementComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiSecurityRequirement sourceSecurityRequirement, + OpenApiSecurityRequirement targetSecurityRequirement, + ComparisonContext comparisonContext) + { + if (sourceSecurityRequirement == null && targetSecurityRequirement == null) + { + return; + } + + if (sourceSecurityRequirement == null || targetSecurityRequirement == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceSecurityRequirement, + TargetValue = targetSecurityRequirement, + OpenApiComparedElementType = typeof(OpenApiSecurityRequirement), + Pointer = comparisonContext.PathString + }); + + return; + } + + var newSecuritySchemesInTarget = targetSecurityRequirement.Keys + .Where(targetReq => sourceSecurityRequirement.Keys.All( + sourceReq => sourceReq.Reference.Id != targetReq.Reference.Id)).ToList(); + + foreach (var newSecuritySchemeInTarget in newSecuritySchemesInTarget) + { + WalkAndAddOpenApiDifference( + comparisonContext, + newSecuritySchemeInTarget.Reference.Id, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = targetSecurityRequirement[newSecuritySchemeInTarget], + OpenApiComparedElementType = typeof(IList) + }); + } + + foreach (var sourceSecurityScheme in sourceSecurityRequirement.Keys) + { + var targetSecurityScheme = + targetSecurityRequirement.Keys.FirstOrDefault( + i => i.Reference.Id == sourceSecurityScheme.Reference.Id); + + if (targetSecurityScheme == null) + { + WalkAndAddOpenApiDifference( + comparisonContext, + sourceSecurityScheme.Reference.Id, + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = sourceSecurityRequirement[sourceSecurityScheme], + OpenApiComparedElementType = typeof(IList) + }); + } + else + { + WalkAndCompare(comparisonContext, + sourceSecurityScheme.Reference.Id, + () => comparisonContext + .GetComparer() + .Compare(sourceSecurityScheme, targetSecurityScheme, comparisonContext)); + } + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiSecuritySchemeComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiSecuritySchemeComparer.cs new file mode 100644 index 000000000..537c7e1b4 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiSecuritySchemeComparer.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiSecuritySchemeComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiSecurityScheme sourcecSecurityScheme, + OpenApiSecurityScheme targetSecurityScheme, + ComparisonContext comparisonContext) + { + if (sourcecSecurityScheme == null && targetSecurityScheme == null) + { + return; + } + + if (sourcecSecurityScheme == null || targetSecurityScheme == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourcecSecurityScheme, + TargetValue = targetSecurityScheme, + OpenApiComparedElementType = typeof(OpenApiSecurityScheme), + Pointer = comparisonContext.PathString + }); + + return; + } + + new OpenApiReferenceComparer() + .Compare(sourcecSecurityScheme.Reference, targetSecurityScheme.Reference, + comparisonContext); + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourcecSecurityScheme.Description, targetSecurityScheme.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Type, + () => Compare(sourcecSecurityScheme.Type, targetSecurityScheme.Type, + comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Name, + () => Compare(sourcecSecurityScheme.Name, targetSecurityScheme.Name, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.In, + () => Compare(sourcecSecurityScheme.In, targetSecurityScheme.In, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Scheme, + () => Compare(sourcecSecurityScheme.Scheme, targetSecurityScheme.Scheme, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.BearerFormat, + () => Compare(sourcecSecurityScheme.BearerFormat, targetSecurityScheme.BearerFormat, + comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.OpenIdConnectUrl, + () => Compare(sourcecSecurityScheme.OpenIdConnectUrl, targetSecurityScheme.OpenIdConnectUrl, + comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Flows, + () => comparisonContext + .GetComparer() + .Compare(sourcecSecurityScheme.Flows, targetSecurityScheme.Flows, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiServerComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiServerComparer.cs new file mode 100644 index 000000000..4ceabd768 --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiServerComparer.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiServerComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiServer sourceServer, + OpenApiServer targetServer, + ComparisonContext comparisonContext) + { + if (sourceServer == null && targetServer == null) + { + return; + } + + if (sourceServer == null || targetServer == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceServer, + TargetValue = targetServer, + OpenApiComparedElementType = typeof(OpenApiServer), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceServer.Description, targetServer.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Url, + () => Compare(sourceServer.Url, targetServer.Url, comparisonContext)); + + WalkAndCompare( + comparisonContext, + OpenApiConstants.Variables, + () => comparisonContext + .GetComparer>() + .Compare(sourceServer.Variables, sourceServer.Variables, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiServerVariableComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiServerVariableComparer.cs new file mode 100644 index 000000000..348366c6e --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiServerVariableComparer.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiServerVariableComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + OpenApiServerVariable sourceServerVariable, + OpenApiServerVariable targetServerVariable, + ComparisonContext comparisonContext) + { + if (sourceServerVariable == null && targetServerVariable == null) + { + return; + } + + if (sourceServerVariable == null || targetServerVariable == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceServerVariable, + TargetValue = targetServerVariable, + OpenApiComparedElementType = typeof(OpenApiServerVariable), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceServerVariable.Description, targetServerVariable.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Default, + () => Compare(sourceServerVariable.Default, targetServerVariable.Default, comparisonContext)); + + // To Do compare enum + // To Do compare extensions + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiServersComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiServersComparer.cs new file mode 100644 index 000000000..7428338cd --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiServersComparer.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of + /// where T is. + /// + public class OpenApiServersComparer : OpenApiComparerBase> + { + /// + /// Executes comparision against source and target + /// where T is. + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare( + IList sourceServers, + IList targetServers, + ComparisonContext comparisonContext) + { + if (sourceServers == null && targetServers == null) + { + return; + } + + if (sourceServers == null || targetServers == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceServers, + TargetValue = targetServers, + OpenApiComparedElementType = typeof(IList), + Pointer = comparisonContext.PathString + }); + + return; + } + + var removedServers = sourceServers.Where( + sourceServer => targetServers.All(targetServer => sourceServer.Url != targetServer.Url)).ToList(); + + for (var i = removedServers.Count - 1; i >= 0; i--) + { + WalkAndAddOpenApiDifference( + comparisonContext, + removedServers.IndexOf(removedServers[i]).ToString(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + SourceValue = removedServers[i], + OpenApiComparedElementType = typeof(OpenApiServer) + }); + } + + var newServersInTarget = targetServers.Where( + targetServer => sourceServers.All(sourceServer => sourceServer.Url != targetServer.Url)).ToList(); + + foreach (var newServerInTarget in newServersInTarget) + { + WalkAndAddOpenApiDifference( + comparisonContext, + targetServers.IndexOf(newServerInTarget).ToString(), + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + TargetValue = newServerInTarget, + OpenApiComparedElementType = typeof(OpenApiServer) + }); + } + + foreach (var sourceServer in sourceServers) + { + var targetServer = targetServers + .FirstOrDefault(server => server.Url == sourceServer.Url); + + if (targetServer == null) + { + continue; + } + + WalkAndCompare( + comparisonContext, + targetServers.IndexOf(targetServer).ToString(), + () => comparisonContext + .GetComparer() + .Compare(sourceServer, targetServer, comparisonContext)); + } + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiTagComparer.cs b/src/Microsoft.OpenApi/Services/OpenApiTagComparer.cs new file mode 100644 index 000000000..bc9eb1d5d --- /dev/null +++ b/src/Microsoft.OpenApi/Services/OpenApiTagComparer.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Services +{ + /// + /// Defines behavior for comparing properties of . + /// + public class OpenApiTagComparer : OpenApiComparerBase + { + /// + /// Executes comparision against source and target . + /// + /// The source. + /// The target. + /// Context under which to compare the source and target. + public override void Compare(OpenApiTag sourceTag, OpenApiTag targetTag, ComparisonContext comparisonContext) + { + if (sourceTag == null && targetTag == null) + { + return; + } + + if (sourceTag == null || targetTag == null) + { + comparisonContext.AddOpenApiDifference( + new OpenApiDifference + { + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + SourceValue = sourceTag, + TargetValue = targetTag, + OpenApiComparedElementType = typeof(OpenApiTag), + Pointer = comparisonContext.PathString + }); + + return; + } + + WalkAndCompare( + comparisonContext, + OpenApiConstants.ExternalDocs, + () => comparisonContext + .GetComparer() + .Compare(sourceTag.ExternalDocs, targetTag.ExternalDocs, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Description, + () => Compare(sourceTag.Description, targetTag.Description, comparisonContext)); + + WalkAndCompare(comparisonContext, OpenApiConstants.Name, + () => Compare(sourceTag.Name, targetTag.Name, comparisonContext)); + } + } +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index 3bf99abab..bc65fdfc2 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -15,7 +15,12 @@ namespace Microsoft.OpenApi.Services public abstract class OpenApiVisitorBase { private readonly Stack _path = new Stack(); - + + /// + /// Properties available to identify context of where an object is within OpenAPI Document + /// + public CurrentKeys CurrentKeys { get; } = new CurrentKeys(); + /// /// Allow Rule to indicate validation error occured at a deeper context level. /// @@ -44,8 +49,6 @@ public string PathString } } - - /// /// Visits /// @@ -273,6 +276,20 @@ public virtual void Visit(OpenApiSecurityRequirement securityRequirement) { } + /// + /// Visits + /// + public virtual void Visit(OpenApiSecurityScheme securityScheme) + { + } + + /// + /// Visits + /// + public virtual void Visit(OpenApiExample example) + { + } + /// /// Visits list of /// @@ -286,7 +303,7 @@ public virtual void Visit(IList openApiTags) public virtual void Visit(IList openApiSecurityRequirements) { } - + /// /// Visits /// @@ -322,5 +339,13 @@ public virtual void Visit(IDictionary serverVaria public virtual void Visit(IDictionary encodings) { } + + /// + /// Visits IOpenApiReferenceable instances that are references and not in components + /// + /// referenced object + public virtual void Visit(IOpenApiReferenceable referenceable) + { + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 0e120037d..dd3c09564 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -18,7 +18,6 @@ public class OpenApiWalker private readonly OpenApiVisitorBase _visitor; private readonly Stack _schemaLoop = new Stack(); private readonly Stack _pathItemLoop = new Stack(); - private bool _inComponents = false; /// /// Initializes the class. @@ -41,11 +40,10 @@ public void Walk(OpenApiDocument doc) _schemaLoop.Clear(); _pathItemLoop.Clear(); - _inComponents = false; _visitor.Visit(doc); - Walk(OpenApiConstants.Info,() => Walk(doc.Info)); + Walk(OpenApiConstants.Info, () => Walk(doc.Info)); Walk(OpenApiConstants.Servers, () => Walk(doc.Servers)); Walk(OpenApiConstants.Paths, () => Walk(doc.Paths)); Walk(OpenApiConstants.Components, () => Walk(doc.Components)); @@ -53,6 +51,7 @@ public void Walk(OpenApiDocument doc) Walk(OpenApiConstants.ExternalDocs, () => Walk(doc.ExternalDocs)); Walk(OpenApiConstants.Tags, () => Walk(doc.Tags)); Walk(doc as IOpenApiExtensible); + } /// @@ -75,7 +74,6 @@ internal void Walk(IList tags) Walk(i.ToString(), () => Walk(tags[i])); } } - } /// @@ -101,8 +99,6 @@ internal void Walk(OpenApiComponents components) return; } - EnterComponents(); - _visitor.Visit(components); if (components == null) @@ -116,7 +112,7 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.Schemas) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); @@ -127,7 +123,7 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.Callbacks) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); @@ -138,7 +134,7 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.Parameters) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); @@ -149,7 +145,7 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.Examples) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); @@ -160,7 +156,7 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.Headers) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); @@ -171,7 +167,7 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.Links) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); @@ -182,7 +178,7 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.RequestBodies) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); @@ -193,13 +189,12 @@ internal void Walk(OpenApiComponents components) { foreach (var item in components.Responses) { - Walk(item.Key, () => Walk(item.Value)); + Walk(item.Key, () => Walk(item.Value, isComponent: true)); } } }); Walk(components as IOpenApiExtensible); - ExitComponents(); } /// @@ -219,7 +214,9 @@ internal void Walk(OpenApiPaths paths) { foreach (var pathItem in paths) { + _visitor.CurrentKeys.Path = pathItem.Key; Walk(pathItem.Key, () => Walk(pathItem.Value));// JSON Pointer uses ~1 as an escape character for / + _visitor.CurrentKeys.Path = null; } } } @@ -241,7 +238,7 @@ internal void Walk(IList servers) { for (int i = 0; i < servers.Count; i++) { - Walk(i.ToString(),() => Walk(servers[i])); + Walk(i.ToString(), () => Walk(servers[i])); } } } @@ -257,7 +254,8 @@ internal void Walk(OpenApiInfo info) } _visitor.Visit(info); - if (info != null) { + if (info != null) + { Walk(OpenApiConstants.Contact, () => Walk(info.Contact)); Walk(OpenApiConstants.License, () => Walk(info.License)); } @@ -280,7 +278,9 @@ internal void Walk(IOpenApiExtensible openApiExtensible) { foreach (var item in openApiExtensible.Extensions) { + _visitor.CurrentKeys.Extension = item.Key; Walk(item.Key, () => Walk(item.Value)); + _visitor.CurrentKeys.Extension = null; } } } @@ -327,9 +327,9 @@ internal void Walk(OpenApiContact contact) /// /// Visits and child objects /// - internal void Walk(OpenApiCallback callback) + internal void Walk(OpenApiCallback callback, bool isComponent = false) { - if (callback == null) + if (callback == null || ProcessAsReference(callback, isComponent)) { return; } @@ -340,8 +340,10 @@ internal void Walk(OpenApiCallback callback) { foreach (var item in callback.PathItems) { + _visitor.CurrentKeys.Callback = item.Key.ToString(); var pathItem = item.Value; Walk(item.Key.ToString(), () => Walk(pathItem)); + _visitor.CurrentKeys.Callback = null; } } } @@ -351,7 +353,7 @@ internal void Walk(OpenApiCallback callback) /// internal void Walk(OpenApiTag tag) { - if (tag == null || IsReference(tag)) + if (tag == null || ProcessAsReference(tag)) { return; } @@ -379,7 +381,7 @@ internal void Walk(OpenApiServer server) /// /// Visits dictionary of /// - internal void Walk(IDictionary serverVariables) + internal void Walk(IDictionary serverVariables) { if (serverVariables == null) { @@ -392,7 +394,9 @@ internal void Walk(IDictionary serverVariables) { foreach (var variable in serverVariables) { + _visitor.CurrentKeys.ServerVariable = variable.Key; Walk(variable.Key, () => Walk(variable.Value)); + _visitor.CurrentKeys.ServerVariable = null; } } } @@ -457,7 +461,9 @@ internal void Walk(IDictionary operations) { foreach (var operation in operations) { + _visitor.CurrentKeys.Operation = operation.Key; Walk(operation.Key.GetDisplayName(), () => Walk(operation.Value)); + _visitor.CurrentKeys.Operation = null; } } } @@ -530,9 +536,9 @@ internal void Walk(IList parameters) /// /// Visits and child objects /// - internal void Walk(OpenApiParameter parameter) + internal void Walk(OpenApiParameter parameter, bool isComponent = false) { - if (parameter == null || IsReference(parameter)) + if (parameter == null || ProcessAsReference(parameter, isComponent)) { return; } @@ -561,7 +567,9 @@ internal void Walk(OpenApiResponses responses) { foreach (var response in responses) { + _visitor.CurrentKeys.Response = response.Key; Walk(response.Key, () => Walk(response.Value)); + _visitor.CurrentKeys.Response = null; } } Walk(responses as IOpenApiExtensible); @@ -570,9 +578,9 @@ internal void Walk(OpenApiResponses responses) /// /// Visits and child objects /// - internal void Walk(OpenApiResponse response) + internal void Walk(OpenApiResponse response, bool isComponent = false) { - if (response == null || IsReference(response)) + if (response == null || ProcessAsReference(response, isComponent)) { return; } @@ -581,16 +589,15 @@ internal void Walk(OpenApiResponse response) Walk(OpenApiConstants.Content, () => Walk(response.Content)); Walk(OpenApiConstants.Links, () => Walk(response.Links)); Walk(OpenApiConstants.Headers, () => Walk(response.Headers)); - Walk(response as IOpenApiExtensible); } /// /// Visits and child objects /// - internal void Walk(OpenApiRequestBody requestBody) + internal void Walk(OpenApiRequestBody requestBody, bool isComponent = false) { - if (requestBody == null || IsReference(requestBody)) + if (requestBody == null || ProcessAsReference(requestBody, isComponent)) { return; } @@ -622,7 +629,9 @@ internal void Walk(IDictionary headers) { foreach (var header in headers) { + _visitor.CurrentKeys.Header = header.Key; Walk(header.Key, () => Walk(header.Value)); + _visitor.CurrentKeys.Header = null; } } } @@ -640,9 +649,11 @@ internal void Walk(IDictionary callbacks) _visitor.Visit(callbacks); if (callbacks != null) { - foreach (var header in callbacks) + foreach (var callback in callbacks) { - Walk(header.Key, () => Walk(header.Value)); + _visitor.CurrentKeys.Callback = callback.Key; + Walk(callback.Key, () => Walk(callback.Value)); + _visitor.CurrentKeys.Callback = null; } } } @@ -662,7 +673,9 @@ internal void Walk(IDictionary content) { foreach (var mediaType in content) { + _visitor.CurrentKeys.Content = mediaType.Key; Walk(mediaType.Key, () => Walk(mediaType.Value)); + _visitor.CurrentKeys.Content = null; } } } @@ -672,13 +685,13 @@ internal void Walk(IDictionary content) /// internal void Walk(OpenApiMediaType mediaType) { - if (mediaType == null) + if (mediaType == null) { return; } _visitor.Visit(mediaType); - + Walk(OpenApiConstants.Example, () => Walk(mediaType.Examples)); Walk(OpenApiConstants.Schema, () => Walk(mediaType.Schema)); Walk(OpenApiConstants.Encoding, () => Walk(mediaType.Encoding)); @@ -701,7 +714,9 @@ internal void Walk(IDictionary encodings) { foreach (var item in encodings) { + _visitor.CurrentKeys.Encoding = item.Key; Walk(item.Key, () => Walk(item.Value)); + _visitor.CurrentKeys.Encoding = null; } } } @@ -723,9 +738,9 @@ internal void Walk(OpenApiEncoding encoding) /// /// Visits and child objects /// - internal void Walk(OpenApiSchema schema) + internal void Walk(OpenApiSchema schema, bool isComponent = false) { - if (schema == null || IsReference(schema)) + if (schema == null || ProcessAsReference(schema, isComponent)) { return; } @@ -733,14 +748,16 @@ internal void Walk(OpenApiSchema schema) if (_schemaLoop.Contains(schema)) { return; // Loop detected, this schema has already been walked. - } else + } + else { _schemaLoop.Push(schema); } _visitor.Visit(schema); - if (schema.Items != null) { + if (schema.Items != null) + { Walk("items", () => Walk(schema.Items)); } @@ -751,10 +768,16 @@ internal void Walk(OpenApiSchema schema) if (schema.AnyOf != null) { - Walk("anyOf", () => Walk(schema.AllOf)); + Walk("anyOf", () => Walk(schema.AnyOf)); } - if (schema.Properties != null) { + if (schema.OneOf != null) + { + Walk("oneOf", () => Walk(schema.OneOf)); + } + + if (schema.Properties != null) + { Walk("properties", () => { foreach (var item in schema.Properties) @@ -774,7 +797,7 @@ internal void Walk(OpenApiSchema schema) /// /// Visits dictionary of /// - internal void Walk(IDictionary examples) + internal void Walk(IDictionary examples) { if (examples == null) { @@ -787,7 +810,9 @@ internal void Walk(IDictionary examples) { foreach (var example in examples) { + _visitor.CurrentKeys.Example = example.Key; Walk(example.Key, () => Walk(example.Value)); + _visitor.CurrentKeys.Example = null; } } } @@ -808,9 +833,9 @@ internal void Walk(IOpenApiAny example) /// /// Visits and child objects /// - internal void Walk(OpenApiExample example) + internal void Walk(OpenApiExample example, bool isComponent = false) { - if (example == null || IsReference(example)) + if (example == null || ProcessAsReference(example, isComponent)) { return; } @@ -891,7 +916,7 @@ internal void Walk(OpenApiOAuthFlow oAuthFlow) /// /// Visits dictionary of and child objects /// - internal void Walk(IDictionary links) + internal void Walk(IDictionary links) { if (links == null) { @@ -904,7 +929,9 @@ internal void Walk(IDictionary links) { foreach (var item in links) { + _visitor.CurrentKeys.Link = item.Key; Walk(item.Key, () => Walk(item.Value)); + _visitor.CurrentKeys.Link = null; } } } @@ -912,9 +939,9 @@ internal void Walk(IDictionary links) /// /// Visits and child objects /// - internal void Walk(OpenApiLink link) + internal void Walk(OpenApiLink link, bool isComponent = false) { - if (link == null || IsReference(link)) + if (link == null || ProcessAsReference(link, isComponent)) { return; } @@ -927,9 +954,9 @@ internal void Walk(OpenApiLink link) /// /// Visits and child objects /// - internal void Walk(OpenApiHeader header) + internal void Walk(OpenApiHeader header, bool isComponent = false) { - if (header == null || IsReference(header)) + if (header == null || ProcessAsReference(header, isComponent)) { return; } @@ -961,7 +988,7 @@ internal void Walk(OpenApiSecurityRequirement securityRequirement) /// internal void Walk(OpenApiSecurityScheme securityScheme) { - if (securityScheme == null || IsReference(securityScheme)) + if (securityScheme == null || ProcessAsReference(securityScheme)) { return; } @@ -970,6 +997,14 @@ internal void Walk(OpenApiSecurityScheme securityScheme) Walk(securityScheme as IOpenApiExtensible); } + /// + /// Visits and child objects + /// + internal void Walk(IOpenApiReferenceable referenceable) + { + _visitor.Visit(referenceable); + } + /// /// Dispatcher method that enables using a single method to walk the model /// starting from any @@ -1030,19 +1065,75 @@ private void Walk(string context, Action walk) /// /// Identify if an element is just a reference to a component, or an actual component /// - private bool IsReference(IOpenApiReferenceable referenceable) + private bool ProcessAsReference(IOpenApiReferenceable referenceable, bool isComponent = false) { - return referenceable.Reference != null && !_inComponents; + var isReference = referenceable.Reference != null && !isComponent; + if (isReference) + { + Walk(referenceable); + } + return isReference; } + } - private void EnterComponents() - { - _inComponents = true; - } + /// + /// Object containing contextual information based on where the walker is currently referencing in an OpenApiDocument + /// + public class CurrentKeys + { + /// + /// Current Path key + /// + public string Path { get; set; } - private void ExitComponents() - { - _inComponents = false; - } + /// + /// Current Operation Type + /// + public OperationType? Operation { get; set; } + + /// + /// Current Response Status Code + /// + public string Response { get; set; } + + /// + /// Current Content Media Type + /// + public string Content { get; set; } + + /// + /// Current Callback Key + /// + public string Callback { get; set; } + + /// + /// Current Link Key + /// + public string Link { get; set; } + + /// + /// Current Header Key + /// + public string Header { get; internal set; } + + /// + /// Current Encoding Key + /// + public string Encoding { get; internal set; } + + /// + /// Current Example Key + /// + public string Example { get; internal set; } + + /// + /// Current Extension Key + /// + public string Extension { get; internal set; } + + /// + /// Current ServerVariable + /// + public string ServerVariable { get; internal set; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Validations/IValidationContext.cs b/src/Microsoft.OpenApi/Validations/IValidationContext.cs index edf19ddaa..58f324bab 100644 --- a/src/Microsoft.OpenApi/Validations/IValidationContext.cs +++ b/src/Microsoft.OpenApi/Validations/IValidationContext.cs @@ -1,19 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.OpenApi.Models; -using Microsoft.OpenApi.Services; -using Microsoft.OpenApi.Validations.Rules; - namespace Microsoft.OpenApi.Validations { /// /// Constrained interface used to provide context to rule implementation /// - public interface IValidationContext + public interface IValidationContext { /// /// Register an error with the validation context. @@ -21,7 +14,6 @@ public interface IValidationContext /// Error to register. void AddError(OpenApiValidatorError error); - /// /// Allow Rule to indicate validation error occured at a deeper context level. /// diff --git a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs index 3bc63d648..69bcda93d 100644 --- a/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs +++ b/src/Microsoft.OpenApi/Validations/OpenApiValidator.cs @@ -13,7 +13,7 @@ namespace Microsoft.OpenApi.Validations /// /// Class containing dispatchers to execute validation rules on for Open API document. /// - public class OpenApiValidator : OpenApiVisitorBase, IValidationContext + public class OpenApiValidator : OpenApiVisitorBase, IValidationContext { private readonly ValidationRuleSet _ruleSet; private readonly IList _errors = new List(); @@ -22,11 +22,11 @@ public class OpenApiValidator : OpenApiVisitorBase, IValidationContext /// Create a vistor that will validate an OpenAPIDocument /// /// - public OpenApiValidator(ValidationRuleSet ruleSet) + public OpenApiValidator(ValidationRuleSet ruleSet) { _ruleSet = ruleSet; } - + /// /// Gets the validation errors. /// @@ -77,12 +77,24 @@ public void AddError(OpenApiValidatorError error) /// The object to be validated public override void Visit(OpenApiComponents item) => Validate(item); + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiHeader item) => Validate(item); + /// /// Execute validation rules against an /// /// The object to be validated public override void Visit(OpenApiResponse item) => Validate(item); + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiMediaType item) => Validate(item); + /// /// Execute validation rules against an /// @@ -113,6 +125,12 @@ public void AddError(OpenApiValidatorError error) /// The object to be validated public override void Visit(OpenApiTag item) => Validate(item); + /// + /// Execute validation rules against an + /// + /// The object to be validated + public override void Visit(OpenApiParameter item) => Validate(item); + /// /// Execute validation rules against an /// @@ -177,7 +195,7 @@ private void Validate(object item, Type type) var potentialReference = item as IOpenApiReferenceable; if (potentialReference != null && potentialReference.UnresolvedReference) { - type = typeof(IOpenApiReferenceable); + type = typeof(IOpenApiReferenceable); } var rules = _ruleSet.FindRules(type); @@ -187,4 +205,4 @@ private void Validate(object item, Type type) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs index 624059705..60267a26d 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiComponentsRules.cs @@ -57,7 +57,7 @@ private static void ValidateKeys(IValidationContext context, IEnumerable { if (!KeyRegex.IsMatch(key)) { - context.CreateError(nameof(KeyMustBeRegularExpression), + context.CreateError(nameof(KeyMustBeRegularExpression), string.Format(SRResource.Validation_ComponentsKeyMustMatchRegularExpr, key, component, KeyRegex.ToString())); } } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs index 6380a7899..f518453f7 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiContactRules.cs @@ -26,7 +26,7 @@ public static class OpenApiContactRules { if (!item.Email.IsEmailAddress()) { - context.CreateError(nameof(EmailMustBeEmailFormat), + context.CreateError(nameof(EmailMustBeEmailFormat), String.Format(SRResource.Validation_StringMustBeEmailAddress, item.Email)); } } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index 789ed5c59..e5193b4c2 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -33,7 +33,7 @@ public static class OpenApiDocumentRules context.Enter("paths"); if (item.Paths == null) { - context.CreateError(nameof(OpenApiDocumentFieldIsMissing), + context.CreateError(nameof(OpenApiDocumentFieldIsMissing), String.Format(SRResource.Validation_FieldIsRequired, "paths", "document")); } context.Exit(); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs new file mode 100644 index 000000000..f2b036457 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiHeaderRules.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiHeaderRules + { + /// + /// Validate the data matches with the given data type. + /// + public static ValidationRule HeaderMismatchedDataType => + new ValidationRule( + (context, header) => + { + // example + context.Enter("example"); + + if (header.Example != null) + { + RuleHelpers.ValidateDataTypeMismatch(context, nameof(HeaderMismatchedDataType), header.Example, header.Schema); + } + + context.Exit(); + + // examples + context.Enter("examples"); + + if (header.Examples != null) + { + foreach (var key in header.Examples.Keys) + { + if (header.Examples[key] != null) + { + context.Enter(key); + context.Enter("value"); + RuleHelpers.ValidateDataTypeMismatch(context, nameof(HeaderMismatchedDataType), header.Examples[key]?.Value, header.Schema); + context.Exit(); + context.Exit(); + } + } + } + + context.Exit(); + }); + + // add more rule. + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs index f96901abe..e02f019e2 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiInfoRules.cs @@ -34,7 +34,7 @@ public static class OpenApiInfoRules context.Enter("version"); if (item.Version == null) { - context.CreateError(nameof(InfoRequiredFields), + context.CreateError(nameof(InfoRequiredFields), String.Format(SRResource.Validation_FieldIsRequired, "version", "info")); } context.Exit(); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs new file mode 100644 index 000000000..d8cb8b69f --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiMediaTypeRules.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiMediaTypeRules + { + /// + /// Validate the data matches with the given data type. + /// + public static ValidationRule MediaTypeMismatchedDataType => + new ValidationRule( + (context, mediaType) => + { + // example + context.Enter("example"); + + if (mediaType.Example != null) + { + RuleHelpers.ValidateDataTypeMismatch(context, nameof(MediaTypeMismatchedDataType), mediaType.Example, mediaType.Schema); + } + + context.Exit(); + + + // enum + context.Enter("examples"); + + if (mediaType.Examples != null) + { + foreach (var key in mediaType.Examples.Keys) + { + if (mediaType.Examples[key] != null) + { + context.Enter(key); + context.Enter("value"); + RuleHelpers.ValidateDataTypeMismatch(context, nameof(MediaTypeMismatchedDataType), mediaType.Examples[key]?.Value, mediaType.Schema); + context.Exit(); + context.Exit(); + } + } + } + + context.Exit(); + }); + + // add more rule. + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs index 37c0dfb1b..7d900e1b4 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiOAuthFlowRules.cs @@ -33,7 +33,7 @@ public static class OpenApiOAuthFlowRules context.Enter("tokenUrl"); if (flow.TokenUrl == null) { - context.CreateError(nameof(OAuthFlowRequiredFields), + context.CreateError(nameof(OAuthFlowRequiredFields), String.Format(SRResource.Validation_FieldIsRequired, "tokenUrl", "OAuth Flow")); } context.Exit(); @@ -42,7 +42,7 @@ public static class OpenApiOAuthFlowRules context.Enter("scopes"); if (flow.Scopes == null) { - context.CreateError(nameof(OAuthFlowRequiredFields), + context.CreateError(nameof(OAuthFlowRequiredFields), String.Format(SRResource.Validation_FieldIsRequired, "scopes", "OAuth Flow")); } context.Exit(); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs new file mode 100644 index 000000000..d1ad41781 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiParameterRules.cs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiParameterRules + { + /// + /// Validate the field is required. + /// + public static ValidationRule ParameterRequiredFields => + new ValidationRule( + (context, item) => + { + // name + context.Enter("name"); + if (item.Name == null) + { + context.CreateError(nameof(ParameterRequiredFields), + String.Format(SRResource.Validation_FieldIsRequired, "name", "parameter")); + } + context.Exit(); + + // in + context.Enter("in"); + if (item.In == null) + { + context.CreateError(nameof(ParameterRequiredFields), + String.Format(SRResource.Validation_FieldIsRequired, "in", "parameter")); + } + context.Exit(); + }); + + /// + /// Validate the "required" field is true when "in" is path. + /// + public static ValidationRule RequiredMustBeTrueWhenInIsPath => + new ValidationRule( + (context, item) => + { + // required + context.Enter("required"); + if (item.In == ParameterLocation.Path && !item.Required) + { + context.CreateError( + nameof(RequiredMustBeTrueWhenInIsPath), + "\"required\" must be true when parameter location is \"path\""); + } + + context.Exit(); + }); + + /// + /// Validate the data matches with the given data type. + /// + public static ValidationRule ParameterMismatchedDataType => + new ValidationRule( + (context, parameter) => + { + // example + context.Enter("example"); + + if (parameter.Example != null) + { + RuleHelpers.ValidateDataTypeMismatch(context, nameof(ParameterMismatchedDataType), parameter.Example, parameter.Schema); + } + + context.Exit(); + + // examples + context.Enter("examples"); + + if (parameter.Examples != null) + { + foreach (var key in parameter.Examples.Keys) + { + if (parameter.Examples[key] != null) + { + context.Enter(key); + context.Enter("value"); + RuleHelpers.ValidateDataTypeMismatch(context, nameof(ParameterMismatchedDataType), parameter.Examples[key]?.Value, parameter.Schema); + context.Exit(); + context.Exit(); + } + } + } + + context.Exit(); + }); + + // add more rule. + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs index 40e5eba6f..7ac374b62 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiPathsRules.cs @@ -24,7 +24,7 @@ public static class OpenApiPathsRules { context.Enter(pathName); - if (pathName == null || !pathName.StartsWith("/")) + if (pathName == null || !pathName.StartsWith("/")) { context.CreateError(nameof(PathNameMustBeginWithSlash), string.Format(SRResource.Validation_PathItemMustBeginWithSlash, pathName)); @@ -36,4 +36,4 @@ public static class OpenApiPathsRules // add more rules } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs index c3bcd2fae..d05a1229c 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiResponsesRules.cs @@ -40,7 +40,7 @@ public static class OpenApiResponsesRules if (key != "default" && !Regex.IsMatch(key, "^[1-5](?>[0-9]{2}|XX)$")) { - context.CreateError(nameof(ResponsesMustBeIdentifiedByDefaultOrStatusCode), + context.CreateError(nameof(ResponsesMustBeIdentifiedByDefaultOrStatusCode), "Responses key must be 'default', an HTTP status code, " + "or one of the following strings representing a range of HTTP status codes: " + "'1XX', '2XX', '3XX', '4XX', '5XX'"); diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs new file mode 100644 index 000000000..8c45c8ff9 --- /dev/null +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiSchemaRules.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using System.Collections.Generic; + +namespace Microsoft.OpenApi.Validations.Rules +{ + /// + /// The validation rules for . + /// + [OpenApiRule] + public static class OpenApiSchemaRules + { + /// + /// Validate the data matches with the given data type. + /// + public static ValidationRule SchemaMismatchedDataType => + new ValidationRule( + (context, schema) => + { + // default + context.Enter("default"); + + if (schema.Default != null) + { + RuleHelpers.ValidateDataTypeMismatch(context, nameof(SchemaMismatchedDataType), schema.Default, schema); + } + + context.Exit(); + + // example + context.Enter("example"); + + if (schema.Example != null) + { + RuleHelpers.ValidateDataTypeMismatch(context, nameof(SchemaMismatchedDataType), schema.Example, schema); + } + + context.Exit(); + + // enum + context.Enter("enum"); + + if (schema.Enum != null) + { + for (int i = 0; i < schema.Enum.Count; i++) + { + context.Enter(i.ToString()); + RuleHelpers.ValidateDataTypeMismatch(context, nameof(SchemaMismatchedDataType), schema.Enum[i], schema); + context.Exit(); + } + } + + context.Exit(); + }); + + /// + /// Validates Schema Discriminator + /// + public static ValidationRule ValidateSchemaDiscriminator => + new ValidationRule( + (context, schema) => + { + // discriminator + context.Enter("discriminator"); + + if (schema.Reference != null && schema.Discriminator != null) + { + if (!schema.Required.Contains(schema.Discriminator?.PropertyName)) + { + context.CreateError(nameof(ValidateSchemaDiscriminator), + string.Format(SRResource.Validation_SchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator, + schema.Reference.Id, schema.Discriminator.PropertyName)); + } + } + + context.Exit(); + }); + } +} diff --git a/src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs b/src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs index 5f369fd9f..05cce7fbc 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/RuleHelpers.cs @@ -2,11 +2,15 @@ // Licensed under the MIT license. using System; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; namespace Microsoft.OpenApi.Validations.Rules { internal static class RuleHelpers { + internal const string DataTypeMismatchedErrorMessage = "Data and type mismatch found."; + /// /// Input string must be in the format of an email address /// @@ -34,5 +38,248 @@ public static bool IsEmailAddress(this string input) return true; } + + public static void ValidateDataTypeMismatch( + IValidationContext context, + string ruleName, + IOpenApiAny value, + OpenApiSchema schema) + { + if (schema == null) + { + return; + } + + var type = schema.Type; + var format = schema.Format; + var nullable = schema.Nullable; + + // Before checking the type, check first if the schema allows null. + // If so and the data given is also null, this is allowed for any type. + if (nullable) + { + if (value is OpenApiNull) + { + return; + } + } + + if (type == "object") + { + // It is not against the spec to have a string representing an object value. + // To represent examples of media types that cannot naturally be represented in JSON or YAML, + // a string value can contain the example with escaping where necessary + if (value is OpenApiString) + { + return; + } + + // If value is not a string and also not an object, there is a data mismatch. + if (!(value is OpenApiObject)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + return; + } + + var anyObject = (OpenApiObject)value; + + foreach (var key in anyObject.Keys) + { + context.Enter(key); + + if (schema.Properties != null && schema.Properties.ContainsKey(key)) + { + ValidateDataTypeMismatch(context, ruleName, anyObject[key], schema.Properties[key]); + } + else + { + ValidateDataTypeMismatch(context, ruleName, anyObject[key], schema.AdditionalProperties); + } + + context.Exit(); + } + + return; + } + + if (type == "array") + { + // It is not against the spec to have a string representing an array value. + // To represent examples of media types that cannot naturally be represented in JSON or YAML, + // a string value can contain the example with escaping where necessary + if (value is OpenApiString) + { + return; + } + + // If value is not a string and also not an array, there is a data mismatch. + if (!(value is OpenApiArray)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + return; + } + + var anyArray = (OpenApiArray)value; + + for (int i = 0; i < anyArray.Count; i++) + { + context.Enter(i.ToString()); + + ValidateDataTypeMismatch(context, ruleName, anyArray[i], schema.Items); + + context.Exit(); + } + + return; + } + + if (type == "integer" && format == "int32") + { + if (!(value is OpenApiInteger)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "integer" && format == "int64") + { + if (!(value is OpenApiLong)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "integer" && !(value is OpenApiInteger)) + { + if (!(value is OpenApiInteger)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "number" && format == "float") + { + if (!(value is OpenApiFloat)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "number" && format == "double") + { + if (!(value is OpenApiDouble)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "number") + { + if (!(value is OpenApiDouble)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "string" && format == "byte") + { + if (!(value is OpenApiByte)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "string" && format == "date") + { + if (!(value is OpenApiDate)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "string" && format == "date-time") + { + if (!(value is OpenApiDateTime)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "string" && format == "password") + { + if (!(value is OpenApiPassword)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "string") + { + if (!(value is OpenApiString)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + + if (type == "boolean") + { + if (!(value is OpenApiBoolean)) + { + context.CreateError( + ruleName, + DataTypeMismatchedErrorMessage); + } + + return; + } + } } } diff --git a/src/Microsoft.OpenApi/Validations/ValidationRule.cs b/src/Microsoft.OpenApi/Validations/ValidationRule.cs index 297691e8c..fdbf5c330 100644 --- a/src/Microsoft.OpenApi/Validations/ValidationRule.cs +++ b/src/Microsoft.OpenApi/Validations/ValidationRule.cs @@ -29,7 +29,7 @@ public abstract class ValidationRule /// Class containing validation rule logic for . /// /// - public class ValidationRule : ValidationRule where T: IOpenApiElement + public class ValidationRule : ValidationRule where T : IOpenApiElement { private readonly Action _validate; diff --git a/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs index c6bc5854d..eca7bc8de 100644 --- a/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs +++ b/src/Microsoft.OpenApi/Validations/ValidationRuleSet.cs @@ -167,7 +167,7 @@ private static ValidationRuleSet BuildDefaultRuleSet() Type validationRuleType = typeof(ValidationRule); IEnumerable rules = typeof(ValidationRuleSet).Assembly.GetTypes() - .Where(t => t.IsClass + .Where(t => t.IsClass && t != typeof(object) && t.GetCustomAttributes(typeof(OpenApiRuleAttribute), false).Any()) .SelectMany(t2 => t2.GetProperties(BindingFlags.Static | BindingFlags.Public) @@ -175,12 +175,12 @@ private static ValidationRuleSet BuildDefaultRuleSet() foreach (var property in rules) { - var propertyValue = property.GetValue(null); // static property - ValidationRule rule = propertyValue as ValidationRule; - if (rule != null) - { - ruleSet.Add(rule); - } + var propertyValue = property.GetValue(null); // static property + ValidationRule rule = propertyValue as ValidationRule; + if (rule != null) + { + ruleSet.Add(rule); + } } return ruleSet; diff --git a/src/Microsoft.OpenApi/Writers/FormattingStreamWriter.cs b/src/Microsoft.OpenApi/Writers/FormattingStreamWriter.cs new file mode 100644 index 000000000..b6db3d1e0 --- /dev/null +++ b/src/Microsoft.OpenApi/Writers/FormattingStreamWriter.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.IO; + +namespace Microsoft.OpenApi.Writers +{ + /// + /// A custom which supports setting a . + /// + public class FormattingStreamWriter : StreamWriter + { + /// + /// Initializes a new instance of the class. + /// + /// + /// + public FormattingStreamWriter(Stream stream, IFormatProvider formatProvider) + : base(stream) + { + this.FormatProvider = formatProvider; + } + + /// + /// The associated with this . + /// + public override IFormatProvider FormatProvider { get; } + } +} diff --git a/src/Microsoft.OpenApi/Writers/IOpenApiWriter.cs b/src/Microsoft.OpenApi/Writers/IOpenApiWriter.cs index 5fc3848d7..8fcbf10ed 100644 --- a/src/Microsoft.OpenApi/Writers/IOpenApiWriter.cs +++ b/src/Microsoft.OpenApi/Writers/IOpenApiWriter.cs @@ -73,4 +73,4 @@ public interface IOpenApiWriter /// void Flush(); } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs b/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs index 1a7276d39..e4c3baa3c 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiJsonWriter.cs @@ -14,18 +14,8 @@ public class OpenApiJsonWriter : OpenApiWriterBase /// Initializes a new instance of the class. /// /// The text writer. - public OpenApiJsonWriter(TextWriter textWriter) - : this(textWriter, new OpenApiSerializerSettings()) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The text writer. - /// The writer settings. - public OpenApiJsonWriter(TextWriter textWriter, OpenApiSerializerSettings settings) - : base(textWriter, settings) + /// Settings for controlling how the OpenAPI document will be written out. + public OpenApiJsonWriter(TextWriter textWriter, OpenApiWriterSettings settings = null) : base(textWriter, settings) { } @@ -215,4 +205,4 @@ public override void WriteRaw(string value) Writer.Write(value); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs index f5551cea5..361da3b2a 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterAnyExtensions.cs @@ -2,10 +2,8 @@ // Licensed under the MIT license. using System.Collections.Generic; -using Microsoft.OpenApi.Interfaces; -using Microsoft.OpenApi.Exceptions; -using Microsoft.OpenApi.Properties; using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Interfaces; namespace Microsoft.OpenApi.Writers { @@ -19,7 +17,8 @@ public static class OpenApiWriterAnyExtensions /// /// The Open API writer. /// The specification extensions. - public static void WriteExtensions(this IOpenApiWriter writer, IDictionary extensions) + /// Version of the OpenAPI specification that that will be output. + public static void WriteExtensions(this IOpenApiWriter writer, IDictionary extensions, OpenApiSpecVersion specVersion) { if (writer == null) { @@ -31,7 +30,7 @@ public static void WriteExtensions(this IOpenApiWriter writer, IDictionary 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. /// @@ -29,23 +36,30 @@ public abstract class OpenApiWriterBase : IOpenApiWriter /// private int _indentLevel; - /// - /// Settings controlling the format and the version of the serialization. - /// - private OpenApiSerializerSettings _settings; - /// /// Initializes a new instance of the class. /// /// The text writer. - /// The writer settings. - public OpenApiWriterBase(TextWriter textWriter, OpenApiSerializerSettings settings) + public OpenApiWriterBase(TextWriter textWriter) { Writer = textWriter; Writer.NewLine = "\n"; Scopes = new Stack(); - this._settings = settings; + } + + /// + /// + /// + /// + /// + public OpenApiWriterBase(TextWriter textWriter, OpenApiWriterSettings settings = null) : this(textWriter) + { + if (settings == null) + { + settings = new OpenApiWriterSettings(); + } + Settings = settings; } /// @@ -112,7 +126,7 @@ public void Flush() /// /// The string value. public abstract void WriteValue(string value); - + /// /// Write float value. /// @@ -164,13 +178,21 @@ public virtual void WriteValue(long value) } /// - /// Write dateTimeOffset value. + /// Write DateTime value. /// - /// The decimal value. + /// The DateTime value. + public virtual void WriteValue(DateTime value) + { + this.WriteValue(value.ToString("o")); + } + + /// + /// Write DateTimeOffset value. + /// + /// The DateTimeOffset value. public virtual void WriteValue(DateTimeOffset value) { - WriteValueSeparator(); - Writer.Write(value.ToString("o")); + this.WriteValue(value.ToString("o")); } /// @@ -225,6 +247,10 @@ public virtual void WriteValue(object value) { WriteValue((decimal)value); } + else if (type == typeof(DateTime) || type == typeof(DateTime?)) + { + WriteValue((DateTime)value); + } else if (type == typeof(DateTimeOffset) || type == typeof(DateTimeOffset?)) { WriteValue((DateTimeOffset)value); @@ -385,4 +411,4 @@ protected void VerifyCanWritePropertyName(string name) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs index 8da814bbb..537273cac 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterExtensions.cs @@ -32,6 +32,26 @@ public static void WriteProperty(this IOpenApiWriter writer, string name, string writer.WriteValue(value); } + /// + /// Write required string property. + /// + /// The writer. + /// The property name. + /// The property value. + public static void WriteRequiredProperty(this IOpenApiWriter writer, string name, string value) + { + CheckArguments(writer, name); + writer.WritePropertyName(name); + if (value == null) + { + writer.WriteNull(); + } + else + { + writer.WriteValue(value); + } + } + /// /// Write a boolean property. /// @@ -414,4 +434,4 @@ private static void CheckArguments(IOpenApiWriter writer, string name) } } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs new file mode 100644 index 000000000..45eedc831 --- /dev/null +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterSettings.cs @@ -0,0 +1,36 @@ + +using Microsoft.OpenApi.Services; + +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 + { + internal LoopDetector LoopDetector { get; } = new LoopDetector(); + /// + /// 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 d6a259514..ee7543b93 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs @@ -14,19 +14,10 @@ public class OpenApiYamlWriter : OpenApiWriterBase /// Initializes a new instance of the class. /// /// The text writer. - public OpenApiYamlWriter(TextWriter textWriter) - : this(textWriter, new OpenApiSerializerSettings()) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The text writer. - /// The writer settings. - public OpenApiYamlWriter(TextWriter textWriter, OpenApiSerializerSettings settings) - : base(textWriter, settings) + /// + public OpenApiYamlWriter(TextWriter textWriter, OpenApiWriterSettings settings = null) : base(textWriter, settings) { + } /// @@ -215,4 +206,4 @@ public override void WriteRaw(string value) Writer.Write(value); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Writers/Scope.cs b/src/Microsoft.OpenApi/Writers/Scope.cs index 2e912e397..37093cffc 100644 --- a/src/Microsoft.OpenApi/Writers/Scope.cs +++ b/src/Microsoft.OpenApi/Writers/Scope.cs @@ -59,4 +59,4 @@ public ScopeType Type /// public bool IsInArray { get; set; } = false; } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Writers/SpecialCharacterStringExtensions.cs b/src/Microsoft.OpenApi/Writers/SpecialCharacterStringExtensions.cs index c9fc3d144..5c6831cb1 100644 --- a/src/Microsoft.OpenApi/Writers/SpecialCharacterStringExtensions.cs +++ b/src/Microsoft.OpenApi/Writers/SpecialCharacterStringExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Globalization; using System.Linq; namespace Microsoft.OpenApi.Writers @@ -54,6 +55,13 @@ public static class SpecialCharacterStringExtensions "," }; + // Plain style strings cannot end with these characters. + // http://www.yaml.org/spec/1.2/spec.html#style/flow/plain + private static readonly string[] _yamlPlainStringForbiddenTerminals = + { + ":" + }; + // Double-quoted strings are needed for these non-printable control characters. // http://www.yaml.org/spec/1.2/spec.html#style/flow/double-quoted private static readonly char[] _yamlControlCharacters = @@ -170,6 +178,7 @@ internal static string GetYamlCompatibleString(this string input) // http://www.yaml.org/spec/1.2/spec.html#style/flow/plain if (_yamlPlainStringForbiddenCombinations.Any(fc => input.Contains(fc)) || _yamlIndicators.Any(i => input.StartsWith(i.ToString())) || + _yamlPlainStringForbiddenTerminals.Any(i => input.EndsWith(i.ToString())) || input.Trim() != input) { // Escape single quotes with two single quotes. @@ -180,7 +189,7 @@ internal static string GetYamlCompatibleString(this string input) // If string can be mistaken as a number, a boolean, or a timestamp, // wrap it in quote to indicate that this is indeed a string, not a number, a boolean, or a timestamp - if (decimal.TryParse(input, out var _) || + if (decimal.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out var _) || bool.TryParse(input, out var _) || DateTime.TryParse(input, out var _)) { @@ -196,6 +205,11 @@ internal static string GetYamlCompatibleString(this string input) /// internal static string GetJsonCompatibleString(this string value) { + if (value == null) + { + return "null"; + } + // Show the control characters as strings // http://json.org/ @@ -212,4 +226,4 @@ internal static string GetJsonCompatibleString(this string value) return $"\"{value}\""; } } -} \ No newline at end of file +} diff --git a/src/Microsoft.OpenApi/Writers/WriterConstants.cs b/src/Microsoft.OpenApi/Writers/WriterConstants.cs index 398ffc2ee..bfc943797 100644 --- a/src/Microsoft.OpenApi/Writers/WriterConstants.cs +++ b/src/Microsoft.OpenApi/Writers/WriterConstants.cs @@ -115,4 +115,4 @@ internal static class WriterConstants /// To indicate empty array in YAML. internal const string EmptyArray = "[ ]"; } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixture.cs b/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixture.cs index ef97f71e3..64e25fcfb 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixture.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixture.cs @@ -19,7 +19,10 @@ public DefaultSettingsFixture() // given that there are multiple types that can be used for the declared type OpenApiAny. // Without this option, properties specific to those types would not be compared. AssertionOptions.AssertEquivalencyUsing( - o => o.AllowingInfiniteRecursion().RespectingRuntimeTypes()); + o => o + .AllowingInfiniteRecursion() + .RespectingRuntimeTypes() + .WithStrictOrdering()); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixtureCollection.cs b/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixtureCollection.cs index 97f8c17fc..1b9bd1107 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixtureCollection.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/DefaultSettingsFixtureCollection.cs @@ -16,4 +16,4 @@ namespace Microsoft.OpenApi.Readers.Tests public class DefaultSettingsFixtureCollection : ICollectionFixture { } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 160b85cc6..7866a1d63 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -13,8 +13,7 @@ ..\..\src\Microsoft.OpenApi.snk - - + @@ -29,6 +28,9 @@ Never + + PreserveNewest + Never @@ -44,12 +46,34 @@ Never + Never Never + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + Never @@ -62,6 +86,11 @@ Never + + + + + Never @@ -74,6 +103,15 @@ Never + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + Never @@ -89,6 +127,9 @@ Never + + Never + Never @@ -108,13 +149,55 @@ Never + Never Never - + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + + + Never + Never @@ -154,16 +237,18 @@ - - + + - + - + - + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -205,7 +290,7 @@ Never - + PreserveNewest diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs index 2004be7b1..1bcd7b9d9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/OpenApiDiagnosticTests.cs @@ -29,8 +29,8 @@ public void DetectedSpecificationVersionShouldBeV3_0() new OpenApiStreamReader().Read(stream, out var diagnostic); diagnostic.Should().NotBeNull(); - diagnostic.SpecificationVersion.Should().Be( OpenApiSpecVersion.OpenApi3_0); + diagnostic.SpecificationVersion.Should().Be(OpenApiSpecVersion.OpenApi3_0); } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/UnsupportedSpecVersionTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/UnsupportedSpecVersionTests.cs index f20529115..cd60aa630 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/UnsupportedSpecVersionTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiReaderTests/UnsupportedSpecVersionTests.cs @@ -22,8 +22,8 @@ public void ThrowOpenApiUnsupportedSpecVersionException() catch (OpenApiUnsupportedSpecVersionException exception) { exception.SpecificationVersion.Should().Be("1.0.0"); - } + } } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/ParseNodeTests.cs b/test/Microsoft.OpenApi.Readers.Tests/ParseNodeTests.cs new file mode 100644 index 000000000..04a400b40 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/ParseNodeTests.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers; +using Microsoft.OpenApi.Readers.Exceptions; +using Xunit; + +namespace Microsoft.OpenApi.Tests +{ + public class ParseNodeTests + { + [Fact] + public void BrokenSimpleList() + { + var input = @"swagger: 2.0 +info: + title: hey + version: 1.0.0 +schemes: [ { ""hello"" }] +paths: { }"; + + var reader = new OpenApiStringReader(); + reader.Read(input, out var diagnostic); + + diagnostic.Errors.Should().BeEquivalentTo(new List() { + new OpenApiError(new OpenApiReaderException("Expected a value.") { + Pointer = "#line=4" + }) + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/OpenApiAnyConverterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/OpenApiAnyConverterTests.cs new file mode 100644 index 000000000..7ee8c3439 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/OpenApiAnyConverterTests.cs @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.ParseNodes; +using SharpYaml.Serialization; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.ParseNodes +{ + [Collection("DefaultSettings")] + public class OpenApiAnyConverterTests + { + [Fact] + public void ParseObjectAsAnyShouldSucceed() + { + var input = @" +aString: fooBar +aInteger: 10 +aDouble: 2.34 +aDateTime: 2017-01-01 +aDate: 2017-01-02 + "; + var yamlStream = new YamlStream(); + yamlStream.Load(new StringReader(input)); + var yamlNode = yamlStream.Documents.First().RootNode; + + var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); + + var node = new MapNode(context, (YamlMappingNode)yamlNode); + + var anyMap = node.CreateAny(); + + var schema = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["aString"] = new OpenApiSchema() + { + Type = "string" + }, + ["aInteger"] = new OpenApiSchema() + { + Type = "integer", + Format = "int32" + }, + ["aDouble"] = new OpenApiSchema() + { + Type = "number", + Format = "double" + }, + ["aDateTime"] = new OpenApiSchema() + { + Type = "string", + Format = "date-time" + }, + ["aDate"] = new OpenApiSchema() + { + Type = "string", + Format = "date" + } + } + }; + + anyMap = OpenApiAnyConverter.GetSpecificOpenApiAny(anyMap, schema); + + diagnostic.Errors.Should().BeEmpty(); + + anyMap.Should().BeEquivalentTo( + new OpenApiObject + { + ["aString"] = new OpenApiString("fooBar"), + ["aInteger"] = new OpenApiInteger(10), + ["aDouble"] = new OpenApiDouble(2.34), + ["aDateTime"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01", CultureInfo.InvariantCulture)), + ["aDate"] = new OpenApiDate(DateTimeOffset.Parse("2017-01-02", CultureInfo.InvariantCulture).Date), + }); + } + + + [Fact] + public void ParseNestedObjectAsAnyShouldSucceed() + { + var input = @" + aString: fooBar + aInteger: 10 + aArray: + - 1 + - 2 + - 3 + aNestedArray: + - aFloat: 1 + aPassword: 1234 + aArray: [abc, def] + aDictionary: + arbitraryProperty: 1 + arbitraryProperty2: 2 + - aFloat: 1.6 + aArray: [123] + aDictionary: + arbitraryProperty: 1 + arbitraryProperty3: 20 + aObject: + aDate: 2017-02-03 + aDouble: 2.34 + aDateTime: 2017-01-01 + "; + var yamlStream = new YamlStream(); + yamlStream.Load(new StringReader(input)); + var yamlNode = yamlStream.Documents.First().RootNode; + + var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); + + var node = new MapNode(context, (YamlMappingNode)yamlNode); + + var anyMap = node.CreateAny(); + + var schema = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["aString"] = new OpenApiSchema() + { + Type = "string" + }, + ["aInteger"] = new OpenApiSchema() + { + Type = "integer", + Format = "int32" + }, + ["aArray"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "integer", + Format = "int64" + } + }, + ["aNestedArray"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["aFloat"] = new OpenApiSchema() + { + Type = "number", + Format = "float" + }, + ["aPassword"] = new OpenApiSchema() + { + Type = "string", + Format = "password" + }, + ["aArray"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "string", + } + }, + ["aDictionary"] = new OpenApiSchema() + { + Type = "object", + AdditionalProperties = new OpenApiSchema() + { + Type = "integer", + Format = "int64" + } + } + } + } + }, + ["aObject"] = new OpenApiSchema() + { + Type = "array", + Properties = + { + ["aDate"] = new OpenApiSchema() + { + Type = "string", + Format = "date" + } + } + }, + ["aDouble"] = new OpenApiSchema() + { + Type = "number", + Format = "double" + }, + ["aDateTime"] = new OpenApiSchema() + { + Type = "string", + Format = "date-time" + } + } + }; + + anyMap = OpenApiAnyConverter.GetSpecificOpenApiAny(anyMap, schema); + + diagnostic.Errors.Should().BeEmpty(); + + anyMap.Should().BeEquivalentTo( + new OpenApiObject + { + ["aString"] = new OpenApiString("fooBar"), + ["aInteger"] = new OpenApiInteger(10), + ["aArray"] = new OpenApiArray() + { + new OpenApiLong(1), + new OpenApiLong(2), + new OpenApiLong(3), + }, + ["aNestedArray"] = new OpenApiArray() + { + new OpenApiObject() + { + ["aFloat"] = new OpenApiFloat(1), + ["aPassword"] = new OpenApiPassword("1234"), + ["aArray"] = new OpenApiArray() + { + new OpenApiString("abc"), + new OpenApiString("def") + }, + ["aDictionary"] = new OpenApiObject() + { + ["arbitraryProperty"] = new OpenApiLong(1), + ["arbitraryProperty2"] = new OpenApiLong(2), + } + }, + new OpenApiObject() + { + ["aFloat"] = new OpenApiFloat((float)1.6), + ["aArray"] = new OpenApiArray() + { + new OpenApiString("123"), + }, + ["aDictionary"] = new OpenApiObject() + { + ["arbitraryProperty"] = new OpenApiLong(1), + ["arbitraryProperty3"] = new OpenApiLong(20), + } + } + }, + ["aObject"] = new OpenApiObject() + { + ["aDate"] = new OpenApiDate(DateTimeOffset.Parse("2017-02-03", CultureInfo.InvariantCulture).Date) + }, + ["aDouble"] = new OpenApiDouble(2.34), + ["aDateTime"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01", CultureInfo.InvariantCulture)) + }); + } + + + [Fact] + public void ParseNestedObjectAsAnyWithPartialSchemaShouldSucceed() + { + var input = @" + aString: fooBar + aInteger: 10 + aArray: + - 1 + - 2 + - 3 + aNestedArray: + - aFloat: 1 + aPassword: 1234 + aArray: [abc, def] + aDictionary: + arbitraryProperty: 1 + arbitraryProperty2: 2 + - aFloat: 1.6 + aArray: [123] + aDictionary: + arbitraryProperty: 1 + arbitraryProperty3: 20 + aObject: + aDate: 2017-02-03 + aDouble: 2.34 + aDateTime: 2017-01-01 + "; + var yamlStream = new YamlStream(); + yamlStream.Load(new StringReader(input)); + var yamlNode = yamlStream.Documents.First().RootNode; + + var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); + + var node = new MapNode(context, (YamlMappingNode)yamlNode); + + var anyMap = node.CreateAny(); + + var schema = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["aString"] = new OpenApiSchema() + { + Type = "string" + }, + ["aArray"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "integer" + } + }, + ["aNestedArray"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["aFloat"] = new OpenApiSchema() + { + }, + ["aPassword"] = new OpenApiSchema() + { + }, + ["aArray"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "string", + } + } + } + } + }, + ["aObject"] = new OpenApiSchema() + { + Type = "array", + Properties = + { + ["aDate"] = new OpenApiSchema() + { + Type = "string" + } + } + }, + ["aDouble"] = new OpenApiSchema() + { + }, + ["aDateTime"] = new OpenApiSchema() + { + } + } + }; + + anyMap = OpenApiAnyConverter.GetSpecificOpenApiAny(anyMap, schema); + + diagnostic.Errors.Should().BeEmpty(); + + anyMap.Should().BeEquivalentTo( + new OpenApiObject + { + ["aString"] = new OpenApiString("fooBar"), + ["aInteger"] = new OpenApiInteger(10), + ["aArray"] = new OpenApiArray() + { + new OpenApiInteger(1), + new OpenApiInteger(2), + new OpenApiInteger(3), + }, + ["aNestedArray"] = new OpenApiArray() + { + new OpenApiObject() + { + ["aFloat"] = new OpenApiInteger(1), + ["aPassword"] = new OpenApiInteger(1234), + ["aArray"] = new OpenApiArray() + { + new OpenApiString("abc"), + new OpenApiString("def") + }, + ["aDictionary"] = new OpenApiObject() + { + ["arbitraryProperty"] = new OpenApiInteger(1), + ["arbitraryProperty2"] = new OpenApiInteger(2), + } + }, + new OpenApiObject() + { + ["aFloat"] = new OpenApiDouble(1.6), + ["aArray"] = new OpenApiArray() + { + new OpenApiString("123"), + }, + ["aDictionary"] = new OpenApiObject() + { + ["arbitraryProperty"] = new OpenApiInteger(1), + ["arbitraryProperty3"] = new OpenApiInteger(20), + } + } + }, + ["aObject"] = new OpenApiObject() + { + ["aDate"] = new OpenApiString("2017-02-03") + }, + ["aDouble"] = new OpenApiDouble(2.34), + ["aDateTime"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01", CultureInfo.InvariantCulture)) + }); + } + + [Fact] + public void ParseNestedObjectAsAnyWithoutUsingSchemaShouldSucceed() + { + var input = @" + aString: fooBar + aInteger: 10 + aArray: + - 1 + - 2 + - 3 + aNestedArray: + - aFloat: 1 + aPassword: 1234 + aArray: [abc, def] + aDictionary: + arbitraryProperty: 1 + arbitraryProperty2: 2 + - aFloat: 1.6 + aArray: [123] + aDictionary: + arbitraryProperty: 1 + arbitraryProperty3: 20 + aObject: + aDate: 2017-02-03 + aDouble: 2.34 + aDateTime: 2017-01-01 + "; + var yamlStream = new YamlStream(); + yamlStream.Load(new StringReader(input)); + var yamlNode = yamlStream.Documents.First().RootNode; + + var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); + + var node = new MapNode(context, (YamlMappingNode)yamlNode); + + var anyMap = node.CreateAny(); + + anyMap = OpenApiAnyConverter.GetSpecificOpenApiAny(anyMap); + + diagnostic.Errors.Should().BeEmpty(); + + anyMap.Should().BeEquivalentTo( + new OpenApiObject + { + ["aString"] = new OpenApiString("fooBar"), + ["aInteger"] = new OpenApiInteger(10), + ["aArray"] = new OpenApiArray() + { + new OpenApiInteger(1), + new OpenApiInteger(2), + new OpenApiInteger(3), + }, + ["aNestedArray"] = new OpenApiArray() + { + new OpenApiObject() + { + ["aFloat"] = new OpenApiInteger(1), + ["aPassword"] = new OpenApiInteger(1234), + ["aArray"] = new OpenApiArray() + { + new OpenApiString("abc"), + new OpenApiString("def") + }, + ["aDictionary"] = new OpenApiObject() + { + ["arbitraryProperty"] = new OpenApiInteger(1), + ["arbitraryProperty2"] = new OpenApiInteger(2), + } + }, + new OpenApiObject() + { + ["aFloat"] = new OpenApiDouble(1.6), + ["aArray"] = new OpenApiArray() + { + new OpenApiInteger(123), + }, + ["aDictionary"] = new OpenApiObject() + { + ["arbitraryProperty"] = new OpenApiInteger(1), + ["arbitraryProperty3"] = new OpenApiInteger(20), + } + } + }, + ["aObject"] = new OpenApiObject() + { + ["aDate"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-02-03", CultureInfo.InvariantCulture)) + }, + ["aDouble"] = new OpenApiDouble(2.34), + ["aDateTime"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01", CultureInfo.InvariantCulture)) + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiAnyTests.cs b/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/OpenApiAnyTests.cs similarity index 68% rename from test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiAnyTests.cs rename to test/Microsoft.OpenApi.Readers.Tests/ParseNodes/OpenApiAnyTests.cs index 86706f9dc..263c28fec 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiAnyTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/ParseNodes/OpenApiAnyTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.IO; using System.Linq; using FluentAssertions; @@ -28,22 +27,22 @@ public void ParseMapAsAnyShouldSucceed() yamlStream.Load(new StringReader(input)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); var anyMap = node.CreateAny(); diagnostic.Errors.Should().BeEmpty(); - anyMap.ShouldBeEquivalentTo( + anyMap.Should().BeEquivalentTo( new OpenApiObject { ["aString"] = new OpenApiString("fooBar"), - ["aInteger"] = new OpenApiInteger(10), - ["aDouble"] = new OpenApiDouble(2.34), - ["aDateTime"] = new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01")) + ["aInteger"] = new OpenApiString("10"), + ["aDouble"] = new OpenApiString("2.34"), + ["aDateTime"] = new OpenApiString("2017-01-01") }); } @@ -60,22 +59,22 @@ public void ParseListAsAnyShouldSucceed() yamlStream.Load(new StringReader(input)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new ListNode(context, diagnostic, (YamlSequenceNode)yamlNode); + var node = new ListNode(context, (YamlSequenceNode)yamlNode); var any = node.CreateAny(); diagnostic.Errors.Should().BeEmpty(); - any.ShouldBeEquivalentTo( + any.Should().BeEquivalentTo( new OpenApiArray { new OpenApiString("fooBar"), - new OpenApiInteger(10), - new OpenApiDouble(2.34), - new OpenApiDateTime(DateTimeOffset.Parse("2017-01-01")) + new OpenApiString("10"), + new OpenApiString("2.34"), + new OpenApiString("2017-01-01") }); } @@ -89,17 +88,17 @@ public void ParseScalarIntegerAsAnyShouldSucceed() yamlStream.Load(new StringReader(input)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new ValueNode(context, diagnostic, (YamlScalarNode)yamlNode); + var node = new ValueNode(context, (YamlScalarNode)yamlNode); var any = node.CreateAny(); diagnostic.Errors.Should().BeEmpty(); - any.ShouldBeEquivalentTo( - new OpenApiInteger(10) + any.Should().BeEquivalentTo( + new OpenApiString("10") ); } @@ -113,18 +112,18 @@ public void ParseScalarDateTimeAsAnyShouldSucceed() yamlStream.Load(new StringReader(input)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new ValueNode(context, diagnostic, (YamlScalarNode)yamlNode); + var node = new ValueNode(context, (YamlScalarNode)yamlNode); var any = node.CreateAny(); diagnostic.Errors.Should().BeEmpty(); - any.ShouldBeEquivalentTo( - new OpenApiDateTime(DateTimeOffset.Parse("2012-07-23T12:33:00")) + any.Should().BeEquivalentTo( + new OpenApiString("2012-07-23T12:33:00") ); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV2Tests.cs b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV2Tests.cs index e5290a693..e374dc205 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV2Tests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV2Tests.cs @@ -101,4 +101,4 @@ public void ParseSecuritySchemeReference() reference.Id.Should().Be(id); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs index 4f34ed562..6acf70500 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs @@ -101,4 +101,4 @@ public void ParseSecuritySchemeReference() reference.Id.Should().Be(id); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/TryLoadReferenceV2Tests.cs b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/TryLoadReferenceV2Tests.cs index d47f7dddc..d7f110b10 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/TryLoadReferenceV2Tests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/TryLoadReferenceV2Tests.cs @@ -41,7 +41,7 @@ public void LoadSchemaReference() var referencedObject = document.ResolveReference(reference); // Assert - referencedObject.ShouldBeEquivalentTo( + referencedObject.Should().BeEquivalentTo( new OpenApiSchema { Required = @@ -96,7 +96,7 @@ public void LoadParameterReference() var referencedObject = document.ResolveReference(reference); // Assert - referencedObject.ShouldBeEquivalentTo( + referencedObject.Should().BeEquivalentTo( new OpenApiParameter { Name = "skip", @@ -139,7 +139,7 @@ public void LoadSecuritySchemeReference() var referencedObject = document.ResolveReference(reference); // Assert - referencedObject.ShouldBeEquivalentTo( + referencedObject.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, @@ -176,7 +176,7 @@ public void LoadResponseReference() var referencedObject = document.ResolveReference(reference); // Assert - referencedObject.ShouldBeEquivalentTo( + referencedObject.Should().BeEquivalentTo( new OpenApiResponse { Description = "Entity not found.", @@ -185,7 +185,7 @@ public void LoadResponseReference() Type = ReferenceType.Response, Id = "NotFound" }, - Content = new Dictionary + Content = new Dictionary { ["application/json"] = new OpenApiMediaType() } @@ -215,7 +215,7 @@ public void LoadResponseAndSchemaReference() var referencedObject = document.ResolveReference(reference); // Assert - referencedObject.ShouldBeEquivalentTo( + referencedObject.Should().BeEquivalentTo( new OpenApiResponse { Description = "General Error", @@ -255,4 +255,4 @@ public void LoadResponseAndSchemaReference() ); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs index a7eddb672..88866fd95 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/TestCustomExtension.cs @@ -26,7 +26,7 @@ public void ParseCustomExtension() "; var settings = new OpenApiReaderSettings() { - ExtensionParsers = { { "x-foo", (a) => { + ExtensionParsers = { { "x-foo", (a,v) => { var fooNode = (OpenApiObject)a; return new FooExtension() { Bar = (fooNode["bar"] as OpenApiString)?.Value, @@ -36,7 +36,7 @@ public void ParseCustomExtension() }; var reader = new OpenApiStringReader(settings); - + var diag = new OpenApiDiagnostic(); var doc = reader.Read(description, out diag); @@ -54,7 +54,7 @@ internal class FooExtension : IOpenApiExtension, IOpenApiElement public string Bar { get; set; } - public void Write(IOpenApiWriter writer) + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { writer.WriteStartObject(); writer.WriteProperty("baz", Baz); diff --git a/test/Microsoft.OpenApi.Readers.Tests/TestHelper.cs b/test/Microsoft.OpenApi.Readers.Tests/TestHelper.cs index 0124d1a7f..c97e35e9b 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/TestHelper.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/TestHelper.cs @@ -16,10 +16,9 @@ public static MapNode CreateYamlMapNode(Stream stream) yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); - var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(new OpenApiDiagnostic()); - return new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + return new MapNode(context, (YamlMappingNode)yamlNode); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/ComparisonTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/ComparisonTests.cs index 6e5538723..1ae3678d6 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/ComparisonTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/ComparisonTests.cs @@ -22,12 +22,12 @@ public void EquivalentV2AndV3DocumentsShouldProductEquivalentObjects(string file using (var streamV3 = Resources.GetStream(Path.Combine(SampleFolderPath, $"{fileName}.v3.yaml"))) { var openApiDocV2 = new OpenApiStreamReader().Read(streamV2, out var diagnosticV2); - var openApiDocV3 = new OpenApiStreamReader().Read(streamV3, out var diagnosticV3 ); + var openApiDocV3 = new OpenApiStreamReader().Read(streamV3, out var diagnosticV3); - openApiDocV3.ShouldBeEquivalentTo(openApiDocV2); + openApiDocV3.Should().BeEquivalentTo(openApiDocV2); - diagnosticV2.Errors.ShouldBeEquivalentTo(diagnosticV3.Errors); + diagnosticV2.Errors.Should().BeEquivalentTo(diagnosticV3.Errors); } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs new file mode 100644 index 000000000..1499dbed7 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs @@ -0,0 +1,442 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Models; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V2Tests +{ + public class OpenApiDocumentTests + { + private const string SampleFolderPath = "V2Tests/Samples/"; + + [Fact] + public void ShouldThrowWhenReferenceTypeIsInvalid() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +paths: + '/': + get: + responses: + '200': + description: ok + schema: + $ref: '#/defi888nition/does/notexist' +"; + + var reader = new OpenApiStringReader(); + var doc = reader.Read(input, out var diagnostic); + + diagnostic.Errors.Should().BeEquivalentTo(new List { + new OpenApiError( new OpenApiException("Unknown reference type 'defi888nition'")) }); + doc.Should().NotBeNull(); + } + + [Fact] + public void ShouldThrowWhenReferenceDoesNotExist() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +paths: + '/': + get: + produces: ['application/json'] + responses: + '200': + description: ok + schema: + $ref: '#/definitions/doesnotexist' +"; + + var reader = new OpenApiStringReader(); + + var doc = reader.Read(input, out var diagnostic); + + diagnostic.Errors.Should().BeEquivalentTo(new List { + new OpenApiError( new OpenApiException("Invalid Reference identifier 'doesnotexist'.")) }); + doc.Should().NotBeNull(); + } + + [Theory] + [InlineData("en-US")] + [InlineData("hi-IN")] + // The equivalent of English 1,000.36 in French and Danish is 1.000,36 + [InlineData("fr-FR")] + [InlineData("da-DK")] + public void ParseDocumentWithDifferentCultureShouldSucceed(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); + + var openApiDoc = new OpenApiStringReader().Read( + @" +swagger: 2.0 +info: + title: Simple Document + version: 0.9.1 + x-extension: 2.335 +definitions: + sampleSchema: + type: object + properties: + sampleProperty: + type: double + minimum: 100.54 + maximum: 60,000,000.35 + exclusiveMaximum: true + exclusiveMinimum: false +paths: {}", + out var context); + + openApiDoc.Should().BeEquivalentTo( + new OpenApiDocument + { + Info = new OpenApiInfo + { + Title = "Simple Document", + Version = "0.9.1", + Extensions = + { + ["x-extension"] = new OpenApiDouble(2.335) + } + }, + Components = new OpenApiComponents() + { + Schemas = + { + ["sampleSchema"] = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["sampleProperty"] = new OpenApiSchema() + { + Type = "double", + Minimum = (decimal)100.54, + Maximum = (decimal)60000000.35, + ExclusiveMaximum = true, + ExclusiveMinimum = false + } + }, + Reference = new OpenApiReference() + { + Id = "sampleSchema", + Type = ReferenceType.Schema + } + } + } + }, + Paths = new OpenApiPaths() + }); + + context.Should().BeEquivalentTo( + new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi2_0 }); + } + + [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); + + var successSchema = new OpenApiSchema() + { + Type = "array", + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "Item" + }, + Items = new OpenApiSchema() + { + //Properties = new Dictionary() + // { + // { "id", new OpenApiSchema() + // { + // Type = "string", + // Description = "Item identifier." + // } + // } + // }, + Reference = new OpenApiReference() + { + Type = ReferenceType.Schema, + Id = "Item" + } + } + }; + + var okSchema = new OpenApiSchema() + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "Item" + }, + Properties = new Dictionary() + { + { "id", new OpenApiSchema() + { + Type = "string", + Description = "Item identifier." + } + } + } + }; + + var errorSchema = new OpenApiSchema() + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "Error" + }, + 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 + } + }; + + var errorMediaType = new OpenApiMediaType + { + Schema = errorSchema + }; + + doc.Should().BeEquivalentTo(new OpenApiDocument + { + Info = new OpenApiInfo + { + Title = "Two responses", + Version = "1.0.0" + }, + Servers = + { + new OpenApiServer + { + Url = "https://" + } + }, + Paths = new OpenApiPaths + { + ["/items"] = new OpenApiPathItem + { + Operations = + { + [OperationType.Get] = new OpenApiOperation + { + Responses = + { + ["200"] = new OpenApiResponse + { + Description = "An OK response", + Content = + { + ["application/json"] = okMediaType, + ["application/xml"] = okMediaType, + } + }, + ["default"] = new OpenApiResponse + { + Description = "An error response", + Content = + { + ["application/json"] = errorMediaType, + ["application/xml"] = errorMediaType + } + } + } + }, + [OperationType.Post] = new OpenApiOperation + { + Responses = + { + ["200"] = new OpenApiResponse + { + Description = "An OK response", + Content = + { + ["html/text"] = okMediaType + } + }, + ["default"] = new OpenApiResponse + { + Description = "An error response", + Content = + { + ["html/text"] = errorMediaType + } + } + } + }, + [OperationType.Patch] = new OpenApiOperation + { + Responses = + { + ["200"] = new OpenApiResponse + { + Description = "An OK response", + Content = + { + ["application/json"] = okMediaType, + ["application/xml"] = okMediaType, + } + }, + ["default"] = new OpenApiResponse + { + Description = "An error response", + Content = + { + ["application/json"] = errorMediaType, + ["application/xml"] = errorMediaType + } + } + } + } + } + } + }, + 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); + } + + Assert.Equal(OpenApiSpecVersion.OpenApi2_0, diagnostic.SpecificationVersion); + + var successSchema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Properties = { + { "id", new OpenApiSchema + { + Type = "string", + Description = "Item identifier." + } + } + }, + Reference = new OpenApiReference + { + Id = "Item", + Type = ReferenceType.Schema + } + } + }; + var errorSchema = new OpenApiSchema + { + Properties = { + { "code", new OpenApiSchema + { + Type = "integer", + Format = "int32" + } + }, + { "message", new OpenApiSchema + { + Type = "string" + } + }, + { "fields", new OpenApiSchema + { + Type = "string" + } + } + }, + Reference = new OpenApiReference + { + Id = "Error", + Type = ReferenceType.Schema + } + }; + var responses = document.Paths["/items"].Operations[OperationType.Get].Responses; + foreach (var response in responses) + { + var targetSchema = response.Key == "200" ? successSchema : errorSchema; + + var json = response.Value.Content["application/json"]; + Assert.NotNull(json); + json.Schema.Should().BeEquivalentTo(targetSchema); + + var xml = response.Value.Content["application/xml"]; + Assert.NotNull(xml); + xml.Schema.Should().BeEquivalentTo(targetSchema); + } + } + + + [Fact] + public void ShouldAllowComponentsThatJustContainAReference() + { + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "ComponentRootReference.json"))) + { + OpenApiStreamReader reader = new OpenApiStreamReader(); + OpenApiDocument doc = reader.Read(stream, out OpenApiDiagnostic diags); + OpenApiSchema schema1 = doc.Components.Schemas["AllPets"]; + Assert.False(schema1.UnresolvedReference); + OpenApiSchema schema2 = (OpenApiSchema)doc.ResolveReference(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"); + } + } + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiHeaderTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiHeaderTests.cs new file mode 100644 index 000000000..7a98c7a6d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiHeaderTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.ParseNodes; +using Microsoft.OpenApi.Readers.V2; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V2Tests +{ + [Collection("DefaultSettings")] + public class OpenApiHeaderTests + { + private const string SampleFolderPath = "V2Tests/Samples/OpenApiHeader/"; + + [Fact] + public void ParseHeaderWithDefaultShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithDefault.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var header = OpenApiV2Deserializer.LoadHeader(node); + + // Assert + header.Should().BeEquivalentTo( + new OpenApiHeader + { + Schema = new OpenApiSchema() + { + Type = "number", + Format = "float", + Default = new OpenApiFloat(5) + } + }); + } + + [Fact] + public void ParseHeaderWithEnumShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerWithEnum.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var header = OpenApiV2Deserializer.LoadHeader(node); + + // Assert + header.Should().BeEquivalentTo( + new OpenApiHeader + { + Schema = new OpenApiSchema() + { + Type = "number", + Format = "float", + Enum = + { + new OpenApiFloat(7), + new OpenApiFloat(8), + new OpenApiFloat(9) + } + } + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs index c655ae5e1..83620aa29 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiOperationTests.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using FluentAssertions; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; @@ -42,7 +43,7 @@ public class OpenApiOperationTests ["200"] = new OpenApiResponse { Description = "Pet updated.", - Content = new Dictionary + Content = new Dictionary { ["application/json"] = new OpenApiMediaType(), ["application/xml"] = new OpenApiMediaType() @@ -219,7 +220,7 @@ public void ParseBasicOperationShouldSucceed() var operation = OpenApiV2Deserializer.LoadOperation(node); // Assert - operation.ShouldBeEquivalentTo(_basicOperation); + operation.Should().BeEquivalentTo(_basicOperation); } [Fact] @@ -237,7 +238,7 @@ public void ParseBasicOperationTwiceShouldYieldSameObject() var operation = OpenApiV2Deserializer.LoadOperation(node); // Assert - operation.ShouldBeEquivalentTo(_basicOperation); + operation.Should().BeEquivalentTo(_basicOperation); } [Fact] @@ -254,7 +255,7 @@ public void ParseOperationWithFormDataShouldSucceed() var operation = OpenApiV2Deserializer.LoadOperation(node); // Assert - operation.ShouldBeEquivalentTo(_operationWithFormData); + operation.Should().BeEquivalentTo(_operationWithFormData); } [Fact] @@ -272,7 +273,7 @@ public void ParseOperationWithFormDataTwiceShouldYieldSameObject() var operation = OpenApiV2Deserializer.LoadOperation(node); // Assert - operation.ShouldBeEquivalentTo(_operationWithFormData); + operation.Should().BeEquivalentTo(_operationWithFormData); } [Fact] @@ -289,7 +290,7 @@ public void ParseOperationWithBodyShouldSucceed() var operation = OpenApiV2Deserializer.LoadOperation(node); // Assert - operation.ShouldBeEquivalentTo(_operationWithBody); + operation.Should().BeEquivalentTo(_operationWithBody); } [Fact] @@ -307,7 +308,68 @@ public void ParseOperationWithBodyTwiceShouldYieldSameObject() var operation = OpenApiV2Deserializer.LoadOperation(node); // Assert - operation.ShouldBeEquivalentTo(_operationWithBody); + operation.Should().BeEquivalentTo(_operationWithBody); + } + + [Fact] + public void ParseOperationWithResponseExamplesShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "operationWithResponseExamples.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var operation = OpenApiV2Deserializer.LoadOperation(node); + + // Assert + operation.Should().BeEquivalentTo( + new OpenApiOperation() + { + Responses = new OpenApiResponses() + { + { "200", new OpenApiResponse() + { + Description = "An array of float response", + Content = + { + ["application/json"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "number", + Format = "float" + } + }, + Example = new OpenApiArray() + { + new OpenApiFloat(5), + new OpenApiFloat(6), + new OpenApiFloat(7), + } + }, + ["application/xml"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "number", + Format = "float" + } + } + } + } + }} + } + } + ); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiParameterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiParameterTests.cs index 0f60937d9..fc4e84f50 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiParameterTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiParameterTests.cs @@ -33,7 +33,7 @@ public void ParseBodyParameterShouldSucceed() // Assert // Body parameter is currently not translated via LoadParameter. // This design may be revisited and this unit test may likely change. - parameter.ShouldBeEquivalentTo(null); + parameter.Should().BeNull(); } [Fact] @@ -50,7 +50,7 @@ public void ParsePathParameterShouldSucceed() var parameter = OpenApiV2Deserializer.LoadParameter(node); // Assert - parameter.ShouldBeEquivalentTo( + parameter.Should().BeEquivalentTo( new OpenApiParameter { In = ParameterLocation.Path, @@ -78,7 +78,7 @@ public void ParseQueryParameterShouldSucceed() var parameter = OpenApiV2Deserializer.LoadParameter(node); // Assert - parameter.ShouldBeEquivalentTo( + parameter.Should().BeEquivalentTo( new OpenApiParameter { In = ParameterLocation.Query, @@ -114,7 +114,7 @@ public void ParseFormDataParameterShouldSucceed() // Assert // Form data parameter is currently not translated via LoadParameter. // This design may be revisited and this unit test may likely change. - parameter.ShouldBeEquivalentTo(null); + parameter.Should().BeNull(); } [Fact] @@ -131,7 +131,7 @@ public void ParseHeaderParameterShouldSucceed() var parameter = OpenApiV2Deserializer.LoadParameter(node); // Assert - parameter.ShouldBeEquivalentTo( + parameter.Should().BeEquivalentTo( new OpenApiParameter { In = ParameterLocation.Header, @@ -139,7 +139,7 @@ public void ParseHeaderParameterShouldSucceed() Description = "token to be passed as a header", Required = true, Style = ParameterStyle.Simple, - + Schema = new OpenApiSchema { Type = "array", @@ -149,24 +149,249 @@ public void ParseHeaderParameterShouldSucceed() Format = "int64", Enum = new List { - new OpenApiInteger(1), - new OpenApiInteger(2), - new OpenApiInteger(3), - new OpenApiInteger(4), + new OpenApiLong(1), + new OpenApiLong(2), + new OpenApiLong(3), + new OpenApiLong(4), } }, Default = new OpenApiArray() { - new OpenApiInteger(1), - new OpenApiInteger(2) + new OpenApiLong(1), + new OpenApiLong(2) }, Enum = new List { - new OpenApiArray() { new OpenApiInteger(1), new OpenApiInteger(2) }, - new OpenApiArray() { new OpenApiInteger(2), new OpenApiInteger(3) }, - new OpenApiArray() { new OpenApiInteger(3), new OpenApiInteger(4) } + new OpenApiArray() { new OpenApiLong(1), new OpenApiLong(2) }, + new OpenApiArray() { new OpenApiLong(2), new OpenApiLong(3) }, + new OpenApiArray() { new OpenApiLong(3), new OpenApiLong(4) } + } + } + }); + } + + [Fact] + public void ParseHeaderParameterWithIncorrectDataTypeShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerParameterWithIncorrectDataType.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV2Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Header, + Name = "token", + Description = "token to be passed as a header", + Required = true, + Style = ParameterStyle.Simple, + + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "string", + Format = "date-time", + Enum = new List + { + new OpenApiString("1"), + new OpenApiString("2"), + new OpenApiString("3"), + new OpenApiString("4"), + } + }, + Default = new OpenApiArray() { + new OpenApiString("1"), + new OpenApiString("2") + }, + Enum = new List + { + new OpenApiArray() { new OpenApiString("1"), new OpenApiString("2") }, + new OpenApiArray() { new OpenApiString("2"), new OpenApiString("3") }, + new OpenApiArray() { new OpenApiString("3"), new OpenApiString("4") } + } + } + }); + } + + [Fact] + public void ParseParameterWithNullLocationShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithNullLocation.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV2Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + + [Fact] + public void ParseParameterWithNoLocationShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithNoLocation.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV2Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + + [Fact] + public void ParseParameterWithNoSchemaShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithNoSchema.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV2Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = false + }); + } + + [Fact] + public void ParseParameterWithUnknownLocationShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithUnknownLocation.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV2Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + + [Fact] + public void ParseParameterWithDefaultShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithDefault.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV2Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Path, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "number", + Format = "float", + Default = new OpenApiFloat(5) + } + }); + } + + [Fact] + public void ParseParameterWithEnumShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithEnum.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV2Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Path, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "number", + Format = "float", + Enum = + { + new OpenApiFloat(7), + new OpenApiFloat(8), + new OpenApiFloat(9) } } }); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiPathItemTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiPathItemTests.cs index 7f0d0f02d..5d3331207 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiPathItemTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiPathItemTests.cs @@ -256,7 +256,7 @@ public void ParseBasicPathItemWithFormDataShouldSucceed() var operation = OpenApiV2Deserializer.LoadPathItem(node); // Assert - operation.ShouldBeEquivalentTo(_basicPathItemWithFormData); + operation.Should().BeEquivalentTo(_basicPathItemWithFormData); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSchemaTests.cs new file mode 100644 index 000000000..9a75e5c8d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSchemaTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.ParseNodes; +using Microsoft.OpenApi.Readers.V2; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V2Tests +{ + [Collection("DefaultSettings")] + public class OpenApiSchemaTests + { + private const string SampleFolderPath = "V2Tests/Samples/OpenApiSchema/"; + + [Fact] + public void ParseSchemaWithDefaultShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "schemaWithDefault.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var schema = OpenApiV2Deserializer.LoadSchema(node); + + // Assert + schema.Should().BeEquivalentTo( + new OpenApiSchema + { + Type = "number", + Format = "float", + Default = new OpenApiFloat(5) + }); + } + + [Fact] + public void ParseSchemaWithExampleShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "schemaWithExample.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var schema = OpenApiV2Deserializer.LoadSchema(node); + + // Assert + schema.Should().BeEquivalentTo( + new OpenApiSchema + { + Type = "number", + Format = "float", + Example = new OpenApiFloat(5) + }); + } + + [Fact] + public void ParseSchemaWithEnumShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "schemaWithEnum.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var schema = OpenApiV2Deserializer.LoadSchema(node); + + // Assert + schema.Should().BeEquivalentTo( + new OpenApiSchema + { + Type = "number", + Format = "float", + Enum = + { + new OpenApiFloat(7), + new OpenApiFloat(8), + new OpenApiFloat(9) + } + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSecuritySchemeTests.cs index 29e04d05f..1a4a2a3d7 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSecuritySchemeTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiSecuritySchemeTests.cs @@ -23,18 +23,18 @@ public void ParseHttpSecuritySchemeShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "basicSecurityScheme.yaml"))) { - var document = OpenApiStreamReader.LoadYamlDocument(stream); + var document = LoadYamlDocument(stream); - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)document.RootNode); + var node = new MapNode(context, (YamlMappingNode)document.RootNode); // Act var securityScheme = OpenApiV2Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, @@ -48,17 +48,17 @@ public void ParseApiKeySecuritySchemeShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "apiKeySecurityScheme.yaml"))) { - var document = OpenApiStreamReader.LoadYamlDocument(stream); - var context = new ParsingContext(); + var document = LoadYamlDocument(stream); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)document.RootNode); + var node = new MapNode(context, (YamlMappingNode)document.RootNode); // Act var securityScheme = OpenApiV2Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, @@ -73,17 +73,17 @@ public void ParseOAuth2ImplicitSecuritySchemeShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "oauth2ImplicitSecurityScheme.yaml"))) { - var document = OpenApiStreamReader.LoadYamlDocument(stream); - var context = new ParsingContext(); + var document = LoadYamlDocument(stream); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)document.RootNode); + var node = new MapNode(context, (YamlMappingNode)document.RootNode); // Act var securityScheme = OpenApiV2Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, @@ -108,17 +108,17 @@ public void ParseOAuth2PasswordSecuritySchemeShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "oauth2PasswordSecurityScheme.yaml"))) { - var document = OpenApiStreamReader.LoadYamlDocument(stream); - var context = new ParsingContext(); + var document = LoadYamlDocument(stream); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)document.RootNode); + var node = new MapNode(context, (YamlMappingNode)document.RootNode); // Act var securityScheme = OpenApiV2Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, @@ -143,17 +143,17 @@ public void ParseOAuth2ApplicationSecuritySchemeShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "oauth2ApplicationSecurityScheme.yaml"))) { - var document = OpenApiStreamReader.LoadYamlDocument(stream); - var context = new ParsingContext(); + var document = LoadYamlDocument(stream); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)document.RootNode); + var node = new MapNode(context, (YamlMappingNode)document.RootNode); // Act var securityScheme = OpenApiV2Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, @@ -178,18 +178,18 @@ public void ParseOAuth2AccessCodeSecuritySchemeShouldSucceed() { using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "oauth2AccessCodeSecurityScheme.yaml"))) { - var document = OpenApiStreamReader.LoadYamlDocument(stream); + var document = LoadYamlDocument(stream); - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)document.RootNode); + var node = new MapNode(context, (YamlMappingNode)document.RootNode); // Act var securityScheme = OpenApiV2Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, @@ -208,5 +208,15 @@ public void ParseOAuth2AccessCodeSecuritySchemeShouldSucceed() }); } } + + static YamlDocument LoadYamlDocument(Stream input) + { + using (var reader = new StreamReader(input)) + { + var yamlStream = new YamlStream(); + yamlStream.Load(reader); + return yamlStream.Documents.First(); + } + } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiServerTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiServerTests.cs new file mode 100644 index 000000000..c87b491ab --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiServerTests.cs @@ -0,0 +1,295 @@ +using FluentAssertions; +using Microsoft.OpenApi.Exceptions; +using Microsoft.OpenApi.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V2Tests +{ + public class OpenApiServerTests + { + [Fact] + public void NoServer() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + }); + + var doc = reader.Read(input, out var diagnostic); + + Assert.Empty(doc.Servers); + } + + [Fact] + public void JustSchemeNoDefault() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +schemes: + - http +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + }); + + var doc = reader.Read(input, out var diagnostic); + + Assert.Equal(0, doc.Servers.Count); + } + + [Fact] + public void JustHostNoDefault() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +host: www.foo.com +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("//www.foo.com", server.Url); + } + + [Fact] + public void JustBasePathNoDefault() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +basePath: /baz +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("/baz", server.Url); + } + + [Fact] + public void JustSchemeWithCustomHost() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +schemes: + - http +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://bing.com/foo") + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("http://bing.com/foo", server.Url); + } + + [Fact] + public void JustSchemeWithCustomHostWithEmptyPath() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +schemes: + - http +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://bing.com") + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("http://bing.com", server.Url); + } + + [Fact] + public void JustBasePathWithCustomHost() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +basePath: /api +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://bing.com") + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("https://bing.com/api", server.Url); + } + + [Fact] + public void JustHostWithCustomHost() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +host: www.example.com +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://bing.com") + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("https://www.example.com", server.Url); + } + + [Fact] + public void JustHostWithCustomHostWithApi() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +host: prod.bing.com +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://dev.bing.com/api") + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("https://prod.bing.com/api", server.Url); + } + + [Fact] + public void MultipleServers() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +schemes: + - http + - https +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://dev.bing.com/api") + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(2, doc.Servers.Count); + Assert.Equal("http://dev.bing.com/api", server.Url); + Assert.Equal("https://dev.bing.com/api", doc.Servers.Last().Url); + } + + [Fact] + public void LocalHostWithCustomHost() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +host: localhost:23232 +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://bing.com") + }); + + var doc = reader.Read(input, out var diagnostic); + + var server = doc.Servers.First(); + Assert.Equal(1, doc.Servers.Count); + Assert.Equal("https://localhost:23232", server.Url); + } + + [Fact] + public void InvalidHostShouldYieldError() + { + var input = @" +swagger: 2.0 +info: + title: test + version: 1.0.0 +host: http://test.microsoft.com +paths: {} +"; + var reader = new OpenApiStringReader(new OpenApiReaderSettings() + { + BaseUrl = new Uri("https://bing.com") + }); + + var doc = reader.Read(input, out var diagnostic); + doc.Servers.Count.Should().Be(0); + diagnostic.Should().BeEquivalentTo( + new OpenApiDiagnostic + { + Errors = + { + new OpenApiError("#/", "Invalid host") + }, + SpecificationVersion = OpenApiSpecVersion.OpenApi2_0 + }); + } + + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/ComponentRootReference.json b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/ComponentRootReference.json new file mode 100644 index 000000000..c5c055e0d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/ComponentRootReference.json @@ -0,0 +1,178 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllPets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "responses": { + "201": { + "description": "Null response" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Info for a specific pet", + "operationId": "showPetById", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to retrieve", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "AllPets": { + "$ref": "#/components/schemas/Pets" + }, + "Error": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithDefault.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithDefault.yaml new file mode 100644 index 000000000..1208cc38b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithDefault.yaml @@ -0,0 +1,3 @@ +type: number +format: float +default: 5 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithEnum.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithEnum.yaml new file mode 100644 index 000000000..5fea6530e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiHeader/headerWithEnum.yaml @@ -0,0 +1,6 @@ +type: number +format: float +enum: + - 7 + - 8 + - 9 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/operationWithResponseExamples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/operationWithResponseExamples.yaml new file mode 100644 index 000000000..725ec5c37 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiOperation/operationWithResponseExamples.yaml @@ -0,0 +1,16 @@ +produces: + - application/json + - application/xml +responses: + 200: + description: An array of float response + schema: + type: array + items: + type: number + format: float + examples: + application/json: + - 5 + - 6 + - 7 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameter.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameter.yaml index e2cab3812..dd1807eea 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameter.yaml +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameter.yaml @@ -1,4 +1,4 @@ -# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject +# modified from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject name: token in: header description: token to be passed as a header diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameterWithIncorrectDataType.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameterWithIncorrectDataType.yaml new file mode 100644 index 000000000..68b69c6f8 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/headerParameterWithIncorrectDataType.yaml @@ -0,0 +1,16 @@ +# modified from https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#parameterObject +name: token +in: header +description: token to be passed as a header +required: true +type: array +default: [1,2] +items: + type: string + format: date-time + enum: [1,2,3,4] +collectionFormat: csv +enum: + - [1,2] + - [2,3] + - [3,4] \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithDefault.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithDefault.yaml new file mode 100644 index 000000000..6ba595251 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithDefault.yaml @@ -0,0 +1,7 @@ +name: username +in: path +description: username to fetch +required: true +default: 5 +type: number +format: float \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithEnum.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithEnum.yaml new file mode 100644 index 000000000..ce9931c82 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithEnum.yaml @@ -0,0 +1,10 @@ +name: username +in: path +description: username to fetch +required: true +enum: + - 7 + - 8 + - 9 +type: number +format: float \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml new file mode 100644 index 000000000..f44ab99ba --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml @@ -0,0 +1,4 @@ +name: username +description: username to fetch +required: true +type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoSchema.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoSchema.yaml new file mode 100644 index 000000000..6ea73cafd --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNoSchema.yaml @@ -0,0 +1,3 @@ +name: username +description: username to fetch +required: false \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml new file mode 100644 index 000000000..399eeb5b6 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml @@ -0,0 +1,5 @@ +name: username +in: null +description: username to fetch +required: true +type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml new file mode 100644 index 000000000..84c380b63 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml @@ -0,0 +1,5 @@ +name: username +in: abcde +description: username to fetch +required: true +type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithDefault.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithDefault.yaml new file mode 100644 index 000000000..1208cc38b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithDefault.yaml @@ -0,0 +1,3 @@ +type: number +format: float +default: 5 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithEnum.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithEnum.yaml new file mode 100644 index 000000000..5fea6530e --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithEnum.yaml @@ -0,0 +1,6 @@ +type: number +format: float +enum: + - 7 + - 8 + - 9 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithExample.yaml b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithExample.yaml new file mode 100644 index 000000000..149cb7331 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/OpenApiSchema/schemaWithExample.yaml @@ -0,0 +1,3 @@ +type: number +format: float +example: 5 \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/multipleProduces.json b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/multipleProduces.json new file mode 100644 index 000000000..81191960c --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/multipleProduces.json @@ -0,0 +1,62 @@ +{ + "swagger": "2.0", + "info": { + "title": "Multiple produces", + "version": "1.0.0" + }, + "schemes": [ + "https" + ], + "basePath": "/", + "paths": { + "/items": { + "get": { + "produces": [ + "application/json", + "application/xml" + ], + "responses": { + "200": { + "description": "An OK response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Item" + } + } + }, + "default": { + "description": "An error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + "definitions": { + "Item": { + "properties": { + "id": { + "type": "string", + "description": "Item identifier." + } + } + }, + "Error": { + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "fields": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/twoResponses.json b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/twoResponses.json new file mode 100644 index 000000000..f822543e1 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/Samples/twoResponses.json @@ -0,0 +1,107 @@ +{ + "swagger": "2.0", + "info": { + "title": "Two responses", + "version": "1.0.0" + }, + "schemes": [ + "https" + ], + "basePath": "/", + "paths": { + "/items": { + "get": { + "produces": [ + "application/json", + "application/xml" + ], + "responses": { + "200": { + "description": "An OK response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Item" + } + } + }, + "default": { + "description": "An error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "responses": { + "200": { + "description": "An OK response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Item" + } + } + }, + "default": { + "description": "An error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "patch": { + "responses": { + "200": { + "description": "An OK response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Item" + } + } + }, + "default": { + "description": "An error response", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "produces": [ + "application/json", + "application/xml" + ] + } + } + }, + "produces": [ + "html/text" + ], + "definitions": { + "Item": { + "properties": { + "id": { + "type": "string", + "description": "Item identifier." + } + } + }, + "Error": { + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "fields": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiCallbackTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiCallbackTests.cs index 29876aad9..f23bee9f9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiCallbackTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiCallbackTests.cs @@ -28,18 +28,18 @@ public void ParseBasicCallbackShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var callback = OpenApiV3Deserializer.LoadCallback(node); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - callback.ShouldBeEquivalentTo( + callback.Should().BeEquivalentTo( new OpenApiCallback { PathItems = @@ -75,9 +75,9 @@ public void ParseBasicCallbackShouldSucceed() } [Fact] - public void ParseAdvancedCallbackWithReferenceShouldSucceed() + public void ParseCallbackWithReferenceShouldSucceed() { - using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "advancedCallbackWithReference.yaml"))) + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "callbackWithReference.yaml"))) { // Act var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); @@ -88,13 +88,71 @@ public void ParseAdvancedCallbackWithReferenceShouldSucceed() var callback = subscribeOperation.Callbacks["simpleHook"]; - diagnostic.ShouldBeEquivalentTo( + diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); - callback.ShouldBeEquivalentTo( + callback.Should().BeEquivalentTo( new OpenApiCallback { - PathItems = + PathItems = + { + [RuntimeExpression.Build("$request.body#/url")]= new OpenApiPathItem { + Operations = { + [OperationType.Post] = new OpenApiOperation() + { + RequestBody = new OpenApiRequestBody + { + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema() + { + Type = "object" + } + } + } + }, + Responses = { + ["200"]= new OpenApiResponse + { + Description = "Success" + } + } + } + } + } + }, + Reference = new OpenApiReference + { + Type = ReferenceType.Callback, + Id = "simpleHook", + } + }); + } + } + + [Fact] + public void ParseMultipleCallbacksWithReferenceShouldSucceed() + { + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "multipleCallbacksWithReference.yaml"))) + { + // Act + var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + + // Assert + var path = openApiDoc.Paths.First().Value; + var subscribeOperation = path.Operations[OperationType.Post]; + + diagnostic.Should().BeEquivalentTo( + new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); + + var callback1 = subscribeOperation.Callbacks["simpleHook"]; + + callback1.Should().BeEquivalentTo( + new OpenApiCallback + { + PathItems = { [RuntimeExpression.Build("$request.body#/url")]= new OpenApiPathItem { Operations = { @@ -129,7 +187,86 @@ public void ParseAdvancedCallbackWithReferenceShouldSucceed() Id = "simpleHook", } }); + + var callback2 = subscribeOperation.Callbacks["callback2"]; + callback2.Should().BeEquivalentTo( + new OpenApiCallback + { + PathItems = + { + [RuntimeExpression.Build("/simplePath")]= new OpenApiPathItem { + Operations = { + [OperationType.Post] = new OpenApiOperation() + { + RequestBody = new OpenApiRequestBody + { + Description = "Callback 2", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema() + { + Type = "string" + } + } + } + }, + Responses = { + ["400"]= new OpenApiResponse + { + Description = "Callback Response" + } + } + } + }, + } + } + }); + + var callback3 = subscribeOperation.Callbacks["callback3"]; + callback3.Should().BeEquivalentTo( + new OpenApiCallback + { + PathItems = + { + [RuntimeExpression.Build(@"http://example.com?transactionId={$request.body#/id}&email={$request.body#/email}")] = new OpenApiPathItem { + Operations = { + [OperationType.Post] = new OpenApiOperation() + { + RequestBody = new OpenApiRequestBody + { + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema() + { + Type = "object" + } + } + } + }, + Responses = { + ["200"]= new OpenApiResponse + { + Description = "Success" + }, + ["401"]= new OpenApiResponse + { + Description = "Unauthorized" + }, + ["404"]= new OpenApiResponse + { + Description = "Not Found" + } + } + } + } + } + } + }); } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs index 70c903dd4..0768592b3 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDiscriminatorTests.cs @@ -26,16 +26,16 @@ public void ParseBasicDiscriminatorShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var discriminator = OpenApiV3Deserializer.LoadDiscriminator(node); // Assert - discriminator.ShouldBeEquivalentTo( + discriminator.Should().BeEquivalentTo( new OpenApiDiscriminator { PropertyName = "pet_type", @@ -48,4 +48,4 @@ public void ParseBasicDiscriminatorShouldSucceed() } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index b8ed90a87..4888abebe 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -3,9 +3,12 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using FluentAssertions; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Validations; using Microsoft.OpenApi.Validations.Rules; @@ -39,17 +42,90 @@ public void ParseDocumentFromInlineStringShouldSucceed() paths: {}", out var context); - openApiDoc.ShouldBeEquivalentTo( + openApiDoc.Should().BeEquivalentTo( new OpenApiDocument { Info = new OpenApiInfo { Title = "Simple Document", Version = "0.9.1" - } + }, + Paths = new OpenApiPaths() + }); + + context.Should().BeEquivalentTo( + new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); + } + + [Theory] + [InlineData("en-US")] + [InlineData("hi-IN")] + // The equivalent of English 1,000.36 in French and Danish is 1.000,36 + [InlineData("fr-FR")] + [InlineData("da-DK")] + public void ParseDocumentWithDifferentCultureShouldSucceed(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); + + var openApiDoc = new OpenApiStringReader().Read( + @" +openapi : 3.0.0 +info: + title: Simple Document + version: 0.9.1 +components: + schemas: + sampleSchema: + type: object + properties: + sampleProperty: + type: double + minimum: 100.54 + maximum: 60,000,000.35 + exclusiveMaximum: true + exclusiveMinimum: false +paths: {}", + out var context); + + openApiDoc.Should().BeEquivalentTo( + new OpenApiDocument + { + Info = new OpenApiInfo + { + Title = "Simple Document", + Version = "0.9.1" + }, + Components = new OpenApiComponents() + { + Schemas = + { + ["sampleSchema"] = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["sampleProperty"] = new OpenApiSchema() + { + Type = "double", + Minimum = (decimal)100.54, + Maximum = (decimal)60000000.35, + ExclusiveMaximum = true, + ExclusiveMinimum = false + } + }, + Reference = new OpenApiReference() + { + Id = "sampleSchema", + Type = ReferenceType.Schema + } + } + } + }, + Paths = new OpenApiPaths() }); - context.ShouldBeEquivalentTo( + context.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); } @@ -60,10 +136,10 @@ public void ParseBasicDocumentWithMultipleServersShouldSucceed() { var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); - diagnostic.ShouldBeEquivalentTo( + diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); - openApiDoc.ShouldBeEquivalentTo( + openApiDoc.Should().BeEquivalentTo( new OpenApiDocument { Info = new OpenApiInfo @@ -83,7 +159,8 @@ public void ParseBasicDocumentWithMultipleServersShouldSucceed() Url = new Uri("https://www.example.org/api").ToString(), Description = "The https endpoint" } - } + }, + Paths = new OpenApiPaths() }); } } @@ -95,16 +172,17 @@ public void ParseBrokenMinimalDocumentShouldYieldExpectedDiagnostic() { var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); - openApiDoc.ShouldBeEquivalentTo( + openApiDoc.Should().BeEquivalentTo( new OpenApiDocument { Info = new OpenApiInfo { Version = "0.9" - } + }, + Paths = new OpenApiPaths() }); - diagnostic.ShouldBeEquivalentTo( + diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic { Errors = @@ -123,17 +201,18 @@ public void ParseMinimalDocumentShouldSucceed() { var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); - openApiDoc.ShouldBeEquivalentTo( + openApiDoc.Should().BeEquivalentTo( new OpenApiDocument { Info = new OpenApiInfo { Title = "Simple Document", Version = "0.9.1" - } + }, + Paths = new OpenApiPaths() }); - diagnostic.ShouldBeEquivalentTo( + diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); } } @@ -558,10 +637,10 @@ public void ParseStandardPetStoreDocumentShouldSucceed() Components = components }; - actual.ShouldBeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } - context.ShouldBeEquivalentTo( + context.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); } @@ -1091,10 +1170,10 @@ public void ParseModifiedPetStoreDocumentWithTagAndSecurityShouldSucceed() } }; - actual.ShouldBeEquivalentTo(expected); + actual.Should().BeEquivalentTo(expected); } - context.ShouldBeEquivalentTo( + context.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); } @@ -1110,7 +1189,7 @@ public void ParsePetStoreExpandedShouldSucceed() // TODO: Create the object in memory and compare with the one read from YAML file. } - context.ShouldBeEquivalentTo( + context.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); } @@ -1126,5 +1205,76 @@ public void GlobalSecurityRequirementShouldReferenceSecurityScheme() Assert.Same(securityRequirement.Keys.First(), openApiDoc.Components.SecuritySchemes.First().Value); } } + + [Fact] + public void HeaderParameterShouldAllowExample() + { + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "apiWithFullHeaderComponent.yaml"))) + { + var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic); + + var exampleHeader = openApiDoc.Components?.Headers?["example-header"]; + Assert.NotNull(exampleHeader); + exampleHeader.Should().BeEquivalentTo( + new OpenApiHeader() + { + Description = "Test header with example", + Required = true, + Deprecated = true, + AllowEmptyValue = true, + AllowReserved = true, + Style = ParameterStyle.Simple, + Explode = true, + Example = new OpenApiString("99391c7e-ad88-49ec-a2ad-99ddcb1f7721"), + Schema = new OpenApiSchema() + { + Type = "string", + Format = "uuid" + }, + Reference = new OpenApiReference() + { + Type = ReferenceType.Header, + Id = "example-header" + } + }); + + var examplesHeader = openApiDoc.Components?.Headers?["examples-header"]; + Assert.NotNull(examplesHeader); + examplesHeader.Should().BeEquivalentTo( + new OpenApiHeader() + { + Description = "Test header with example", + Required = true, + Deprecated = true, + AllowEmptyValue = true, + AllowReserved = true, + Style = ParameterStyle.Simple, + Explode = true, + Examples = new Dictionary() + { + { "uuid1", new OpenApiExample() + { + Value = new OpenApiString("99391c7e-ad88-49ec-a2ad-99ddcb1f7721") + } + }, + { "uuid2", new OpenApiExample() + { + Value = new OpenApiString("99391c7e-ad88-49ec-a2ad-99ddcb1f7721") + } + } + }, + Schema = new OpenApiSchema() + { + Type = "string", + Format = "uuid" + }, + Reference = new OpenApiReference() + { + Type = ReferenceType.Header, + Id = "examples-header" + } + }); + } + } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiEncodingTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiEncodingTests.cs index 24f8af6a3..7f33491ff 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiEncodingTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiEncodingTests.cs @@ -26,16 +26,16 @@ public void ParseBasicEncodingShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var encoding = OpenApiV3Deserializer.LoadEncoding(node); // Assert - encoding.ShouldBeEquivalentTo( + encoding.Should().BeEquivalentTo( new OpenApiEncoding { ContentType = "application/xml; charset=utf-8" @@ -52,16 +52,16 @@ public void ParseAdvancedEncodingShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var encoding = OpenApiV3Deserializer.LoadEncoding(node); // Assert - encoding.ShouldBeEquivalentTo( + encoding.Should().BeEquivalentTo( new OpenApiEncoding { ContentType = "image/png, image/jpeg", @@ -81,4 +81,4 @@ public void ParseAdvancedEncodingShouldSucceed() } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs index 20e3f33eb..ead84f201 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiExampleTests.cs @@ -27,16 +27,16 @@ public void ParseAdvancedExampleShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); var example = OpenApiV3Deserializer.LoadExample(node); diagnostic.Errors.Should().BeEmpty(); - example.ShouldBeEquivalentTo( + example.Should().BeEquivalentTo( new OpenApiExample { Value = new OpenApiObject @@ -75,5 +75,15 @@ public void ParseAdvancedExampleShouldSucceed() }); } } + + [Fact] + public void ParseExampleForcedStringSucceed() + { + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "explicitString.yaml"))) + { + new OpenApiStreamReader().Read(stream, out var diagnostic); + diagnostic.Errors.Should().BeEmpty(); + } + } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs index c9e53f8c9..29b23cb31 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiInfoTests.cs @@ -28,16 +28,16 @@ public void ParseAdvancedInfoShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var openApiInfo = OpenApiV3Deserializer.LoadInfo(node); // Assert - openApiInfo.ShouldBeEquivalentTo( + openApiInfo.Should().BeEquivalentTo( new OpenApiInfo { Title = "Advanced Info", @@ -56,7 +56,7 @@ public void ParseAdvancedInfoShouldSucceed() }, License = new OpenApiLicense { - Extensions = {["x-disclaimer"] = new OpenApiString("Sample Extension String Disclaimer")}, + Extensions = { ["x-disclaimer"] = new OpenApiString("Sample Extension String Disclaimer") }, Name = "licenseName", Url = new Uri("http://www.example.com/url2") }, @@ -71,8 +71,8 @@ public void ParseAdvancedInfoShouldSucceed() }, ["x-list"] = new OpenApiArray { - new OpenApiInteger(1), - new OpenApiInteger(2) + new OpenApiString("1"), + new OpenApiString("2") } } }); @@ -88,16 +88,16 @@ public void ParseBasicInfoShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var openApiInfo = OpenApiV3Deserializer.LoadInfo(node); // Assert - openApiInfo.ShouldBeEquivalentTo( + openApiInfo.Should().BeEquivalentTo( new OpenApiInfo { Title = "Basic Info", @@ -128,16 +128,16 @@ public void ParseMinimalInfoShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var openApiInfo = OpenApiV3Deserializer.LoadInfo(node); // Assert - openApiInfo.ShouldBeEquivalentTo( + openApiInfo.Should().BeEquivalentTo( new OpenApiInfo { Title = "Minimal Info", @@ -146,4 +146,4 @@ public void ParseMinimalInfoShouldSucceed() } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs new file mode 100644 index 000000000..e62eabb53 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiMediaTypeTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.ParseNodes; +using Microsoft.OpenApi.Readers.V3; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V3Tests +{ + [Collection("DefaultSettings")] + public class OpenApiMediaTypeTests + { + private const string SampleFolderPath = "V3Tests/Samples/OpenApiMediaType/"; + + [Fact] + public void ParseMediaTypeWithExampleShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "mediaTypeWithExample.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var mediaType = OpenApiV3Deserializer.LoadMediaType(node); + + // Assert + mediaType.Should().BeEquivalentTo( + new OpenApiMediaType + { + Example = new OpenApiFloat(5), + Schema = new OpenApiSchema + { + Type = "number", + Format = "float" + } + }); + } + + [Fact] + public void ParseMediaTypeWithExamplesShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "mediaTypeWithExamples.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var mediaType = OpenApiV3Deserializer.LoadMediaType(node); + + // Assert + mediaType.Should().BeEquivalentTo( + new OpenApiMediaType + { + Examples = + { + ["example1"] = new OpenApiExample() + { + Value = new OpenApiFloat(5), + }, + ["example2"] = new OpenApiExample() + { + Value = new OpenApiFloat((float)7.5), + } + }, + Schema = new OpenApiSchema + { + Type = "number", + Format = "float" + } + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiOperationTests.cs index 29001c22c..a74c64154 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiOperationTests.cs @@ -3,6 +3,10 @@ using System.IO; using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.ParseNodes; +using Microsoft.OpenApi.Readers.V3; using Xunit; namespace Microsoft.OpenApi.Readers.Tests.V3Tests @@ -24,6 +28,62 @@ public void OperationWithSecurityRequirementShouldReferenceSecurityScheme() } } - + [Fact] + public void ParseOperationWithParameterWithNoLocationShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "operationWithParameterWithNoLocation.json"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var operation = OpenApiV3Deserializer.LoadOperation(node); + + // Assert + operation.Should().BeEquivalentTo(new OpenApiOperation() + { + Tags = + { + new OpenApiTag + { + UnresolvedReference = true, + Reference = new OpenApiReference() + { + Id = "user", + Type = ReferenceType.Tag + } + } + }, + Summary = "Logs user into the system", + Description = "", + OperationId = "loginUser", + Parameters = + { + new OpenApiParameter + { + Name = "username", + Description = "The user name for login", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }, + new OpenApiParameter + { + Name = "password", + Description = "The password for login in clear text", + In = ParameterLocation.Query, + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiParameterTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiParameterTests.cs new file mode 100644 index 000000000..44ba3316d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiParameterTests.cs @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.ParseNodes; +using Microsoft.OpenApi.Readers.V3; +using Xunit; + +namespace Microsoft.OpenApi.Readers.Tests.V3Tests +{ + [Collection("DefaultSettings")] + public class OpenApiParameterTests + { + private const string SampleFolderPath = "V3Tests/Samples/OpenApiParameter/"; + + [Fact] + public void ParsePathParameterShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "pathParameter.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Path, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + + [Fact] + public void ParseQueryParameterShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "queryParameter.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Query, + Name = "id", + Description = "ID of the object to fetch", + Required = false, + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "string" + } + }, + Style = ParameterStyle.Form, + Explode = true + }); + } + + [Fact] + public void ParseQueryParameterWithObjectTypeShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "queryParameterWithObjectType.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Query, + Name = "freeForm", + Schema = new OpenApiSchema + { + Type = "object", + AdditionalProperties = new OpenApiSchema + { + Type = "integer" + } + }, + Style = ParameterStyle.Form + }); + } + + [Fact] + public void ParseQueryParameterWithObjectTypeAndContentShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "queryParameterWithObjectTypeAndContent.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Query, + Name = "coordinates", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "object", + Required = + { + "lat", + "long" + }, + Properties = + { + ["lat"] = new OpenApiSchema + { + Type = "number" + }, + ["long"] = new OpenApiSchema + { + Type = "number" + } + } + } + } + } + }); + } + + [Fact] + public void ParseHeaderParameterShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "headerParameter.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = ParameterLocation.Header, + Name = "token", + Description = "token to be passed as a header", + Required = true, + Style = ParameterStyle.Simple, + + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "integer", + Format = "int64", + } + } + }); + } + + [Fact] + public void ParseParameterWithNullLocationShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithNullLocation.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + + [Fact] + public void ParseParameterWithNoLocationShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithNoLocation.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + + [Fact] + public void ParseParameterWithUnknownLocationShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithUnknownLocation.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Schema = new OpenApiSchema + { + Type = "string" + } + }); + } + + [Fact] + public void ParseParameterWithExampleShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithExample.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Example = new OpenApiFloat(5), + Schema = new OpenApiSchema + { + Type = "number", + Format = "float" + } + }); + } + + [Fact] + public void ParseParameterWithExamplesShouldSucceed() + { + // Arrange + MapNode node; + using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "parameterWithExamples.yaml"))) + { + node = TestHelper.CreateYamlMapNode(stream); + } + + // Act + var parameter = OpenApiV3Deserializer.LoadParameter(node); + + // Assert + parameter.Should().BeEquivalentTo( + new OpenApiParameter + { + In = null, + Name = "username", + Description = "username to fetch", + Required = true, + Examples = + { + ["example1"] = new OpenApiExample() + { + Value = new OpenApiFloat(5), + }, + ["example2"] = new OpenApiExample() + { + Value = new OpenApiFloat((float)7.5), + } + }, + Schema = new OpenApiSchema + { + Type = "number", + Format = "float" + } + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs index 7fdc2145d..dbf0cf3f6 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs @@ -28,18 +28,18 @@ public void ParsePrimitiveSchemaShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var schema = OpenApiV3Deserializer.LoadSchema(node); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - schema.ShouldBeEquivalentTo( + schema.Should().BeEquivalentTo( new OpenApiSchema { Type = "string", @@ -60,9 +60,9 @@ public void ParsePrimitiveSchemaFragmentShouldSucceed() var schema = reader.ReadFragment(stream, OpenApiSpecVersion.OpenApi3_0, out diagnostic); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - schema.ShouldBeEquivalentTo( + schema.Should().BeEquivalentTo( new OpenApiSchema { Type = "string", @@ -87,14 +87,14 @@ public void ParsePrimitiveStringSchemaFragmentShouldSucceed() var schema = reader.ReadFragment(input, OpenApiSpecVersion.OpenApi3_0, out diagnostic); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - schema.ShouldBeEquivalentTo( + schema.Should().BeEquivalentTo( new OpenApiSchema { Type = "integer", Format = "int64", - Default = new OpenApiInteger(88) + Default = new OpenApiLong(88) }); } @@ -113,15 +113,15 @@ public void ParseExampleStringFragmentShouldSucceed() var openApiAny = reader.ReadFragment(input, OpenApiSpecVersion.OpenApi3_0, out diagnostic); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - openApiAny.ShouldBeEquivalentTo( + openApiAny.Should().BeEquivalentTo( new OpenApiObject { ["foo"] = new OpenApiString("bar"), - ["baz"] = new OpenApiArray() { - new OpenApiInteger(1), - new OpenApiInteger(2) + ["baz"] = new OpenApiArray() { + new OpenApiInteger(1), + new OpenApiInteger(2) } }); } @@ -141,9 +141,9 @@ public void ParseEnumFragmentShouldSucceed() var openApiAny = reader.ReadFragment(input, OpenApiSpecVersion.OpenApi3_0, out diagnostic); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - openApiAny.ShouldBeEquivalentTo( + openApiAny.Should().BeEquivalentTo( new OpenApiArray { new OpenApiString("foo"), @@ -160,18 +160,18 @@ public void ParseSimpleSchemaShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var schema = OpenApiV3Deserializer.LoadSchema(node); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - schema.ShouldBeEquivalentTo( + schema.Should().BeEquivalentTo( new OpenApiSchema { Type = "object", @@ -196,7 +196,7 @@ public void ParseSimpleSchemaShouldSucceed() Minimum = 0 } }, - AdditionalPropertiesAllowed = false + AdditionalPropertiesAllowed = false }); } } @@ -218,9 +218,9 @@ public void ParsePathFragmentShouldSucceed() var openApiAny = reader.ReadFragment(input, OpenApiSpecVersion.OpenApi3_0, out diagnostic); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - openApiAny.ShouldBeEquivalentTo( + openApiAny.Should().BeEquivalentTo( new OpenApiPathItem { Summary = "externally referenced path item", @@ -230,8 +230,9 @@ public void ParsePathFragmentShouldSucceed() { Responses = new OpenApiResponses { - ["200"] = new OpenApiResponse { - Description = "Ok" + ["200"] = new OpenApiResponse + { + Description = "Ok" } } } @@ -248,18 +249,18 @@ public void ParseDictionarySchemaShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var schema = OpenApiV3Deserializer.LoadSchema(node); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - schema.ShouldBeEquivalentTo( + schema.Should().BeEquivalentTo( new OpenApiSchema { Type = "object", @@ -280,18 +281,18 @@ public void ParseBasicSchemaWithExampleShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var schema = OpenApiV3Deserializer.LoadSchema(node); // Assert - diagnostic.ShouldBeEquivalentTo(new OpenApiDiagnostic()); + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic()); - schema.ShouldBeEquivalentTo( + schema.Should().BeEquivalentTo( new OpenApiSchema { Type = "object", @@ -314,7 +315,7 @@ public void ParseBasicSchemaWithExampleShouldSucceed() Example = new OpenApiObject { ["name"] = new OpenApiString("Puma"), - ["id"] = new OpenApiInteger(1) + ["id"] = new OpenApiLong(1) } }); } @@ -331,10 +332,10 @@ public void ParseBasicSchemaWithReferenceShouldSucceed() // Assert var components = openApiDoc.Components; - diagnostic.ShouldBeEquivalentTo( + diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); - components.ShouldBeEquivalentTo( + components.Should().BeEquivalentTo( new OpenApiComponents { Schemas = @@ -434,10 +435,10 @@ public void ParseAdvancedSchemaWithReferenceShouldSucceed() // Assert var components = openApiDoc.Components; - diagnostic.ShouldBeEquivalentTo( + diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); - components.ShouldBeEquivalentTo( + components.Should().BeEquivalentTo( new OpenApiComponents { Schemas = @@ -464,7 +465,7 @@ public void ParseAdvancedSchemaWithReferenceShouldSucceed() { "name", "petType" - }, + }, Reference = new OpenApiReference() { Id= "Pet", @@ -610,7 +611,7 @@ public void ParseSelfReferencingSchemaShouldNotStackOverflow() // Assert var components = openApiDoc.Components; - diagnostic.ShouldBeEquivalentTo( + diagnostic.Should().BeEquivalentTo( new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); var schemaExtension = new OpenApiSchema() @@ -642,8 +643,8 @@ public void ParseSelfReferencingSchemaShouldNotStackOverflow() schemaExtension.AllOf[0].Properties["child"] = schemaExtension; - components.Schemas["microsoft.graph.schemaExtension"].ShouldBeEquivalentTo(components.Schemas["microsoft.graph.schemaExtension"].AllOf[0].Properties["child"]); + components.Schemas["microsoft.graph.schemaExtension"].Should().BeEquivalentTo(components.Schemas["microsoft.graph.schemaExtension"].AllOf[0].Properties["child"]); } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSecuritySchemeTests.cs index 0765d7272..57c156cc0 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSecuritySchemeTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSecuritySchemeTests.cs @@ -27,16 +27,16 @@ public void ParseHttpSecuritySchemeShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var securityScheme = OpenApiV3Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, @@ -54,16 +54,16 @@ public void ParseApiKeySecuritySchemeShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var securityScheme = OpenApiV3Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.ApiKey, @@ -82,16 +82,16 @@ public void ParseBearerSecuritySchemeShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var securityScheme = OpenApiV3Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.Http, @@ -110,16 +110,16 @@ public void ParseOAuth2SecuritySchemeShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var securityScheme = OpenApiV3Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, @@ -148,16 +148,16 @@ public void ParseOpenIdConnectSecuritySchemeShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var securityScheme = OpenApiV3Deserializer.LoadSecurityScheme(node); // Assert - securityScheme.ShouldBeEquivalentTo( + securityScheme.Should().BeEquivalentTo( new OpenApiSecurityScheme { Type = SecuritySchemeType.OpenIdConnect, @@ -167,4 +167,4 @@ public void ParseOpenIdConnectSecuritySchemeShouldSucceed() } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs index 15218ac77..a10d674a9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiXmlTests.cs @@ -27,16 +27,16 @@ public void ParseBasicXmlShouldSucceed() yamlStream.Load(new StreamReader(stream)); var yamlNode = yamlStream.Documents.First().RootNode; - var context = new ParsingContext(); var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic); - var node = new MapNode(context, diagnostic, (YamlMappingNode)yamlNode); + var node = new MapNode(context, (YamlMappingNode)yamlNode); // Act var xml = OpenApiV3Deserializer.LoadXml(node); // Assert - xml.ShouldBeEquivalentTo( + xml.Should().BeEquivalentTo( new OpenApiXml { Name = "name1", @@ -47,4 +47,4 @@ public void ParseBasicXmlShouldSucceed() } } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/advancedCallbackWithReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/callbackWithReference.yaml similarity index 100% rename from test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/advancedCallbackWithReference.yaml rename to test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/callbackWithReference.yaml diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/multipleCallbacksWithReference.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/multipleCallbacksWithReference.yaml new file mode 100644 index 000000000..346416688 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiCallback/multipleCallbacksWithReference.yaml @@ -0,0 +1,58 @@ +openapi : 3.0.0 +info: + title: The API + version: 0.9.1 +paths: + "/subscribe": + post: + requestBody: + content: + application/json: + schema: + type: object + responses: + 200: + description: Register + callbacks: + simpleHook: + $ref: "#/components/callbacks/simpleHook" + callback2: + "/simplePath": + post: + requestBody: + description: Callback 2 + content: + application/json: + schema: + type: string + responses: + 400: + description: Callback Response + callback3: + "http://example.com?transactionId={$request.body#/id}&email={$request.body#/email}": + post: + requestBody: + content: + application/xml: + schema: + type: object + responses: + 200: + description: Success + 401: + description: Unauthorized + 404: + description: Not Found +components: + callbacks: + simpleHook: + '$request.body#/url': + post: + requestBody: + content: + application/json: + schema: + type: object + responses: + 200: + description: Success diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/apiWithFullHeaderComponent.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/apiWithFullHeaderComponent.yaml new file mode 100644 index 000000000..ca3b56c73 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/apiWithFullHeaderComponent.yaml @@ -0,0 +1,34 @@ +openapi: '3.0.0' +info: + version: '1.0.0' + title: Header components +components: + headers: + example-header: + description: Test header with example + required: true + deprecated: true + allowEmptyValue: true + allowReserved: true + style: simple + explode: true + example: "99391c7e-ad88-49ec-a2ad-99ddcb1f7721" + schema: + type: string + format: uuid + examples-header: + description: Test header with example + required: true + deprecated: true + allowEmptyValue: true + allowReserved: true + style: simple + explode: true + examples: + uuid1: + value: "99391c7e-ad88-49ec-a2ad-99ddcb1f7721" + uuid2: + value: "99391c7e-ad88-49ec-a2ad-99ddcb1f7721" + schema: + type: string + format: uuid \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiExample/explicitString.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiExample/explicitString.yaml new file mode 100644 index 000000000..c3103a810 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiExample/explicitString.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Test API +paths: + /test-path: + post: + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/test-schema' + responses: + default: + description: '' +components: + schemas: + test-schema: + type: object + properties: + sub: + $ref: '#/components/schemas/test-sub-schema' + example: + sub: + test-property1: "12345" + test-property2: "1970-01-01T00:00:00Z" + test-sub-schema: + type: object + properties: + test-property1: + type: string + example: "12345" + test-property2: + type: string + format: date-time + example: "1970-01-01T00:00:00Z" \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExample.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExample.yaml new file mode 100644 index 000000000..180ff0526 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExample.yaml @@ -0,0 +1,8 @@ +name: username +in: null +description: username to fetch +required: true +example: 5 +schema: + type: number + format: float \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExamples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExamples.yaml new file mode 100644 index 000000000..2fcf82c24 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiMediaType/mediaTypeWithExamples.yaml @@ -0,0 +1,12 @@ +name: username +in: null +description: username to fetch +required: true +examples: + example1: + value: 5 + example2: + value: 7.5 +schema: + type: number + format: float \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiOperation/operationWithParameterWithNoLocation.json b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiOperation/operationWithParameterWithNoLocation.json new file mode 100644 index 000000000..6d15720e3 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiOperation/operationWithParameterWithNoLocation.json @@ -0,0 +1,25 @@ +{ + "tags": [ "user" ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "description": "The user name for login", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "schema": { + "type": "string" + } + } + ] +} diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/headerParameter.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/headerParameter.yaml new file mode 100644 index 000000000..c0f1964de --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/headerParameter.yaml @@ -0,0 +1,11 @@ +# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-object-examples +name: token +in: header +description: token to be passed as a header +required: true +schema: + type: array + items: + type: integer + format: int64 +style: simple \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExample.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExample.yaml new file mode 100644 index 000000000..180ff0526 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExample.yaml @@ -0,0 +1,8 @@ +name: username +in: null +description: username to fetch +required: true +example: 5 +schema: + type: number + format: float \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExamples.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExamples.yaml new file mode 100644 index 000000000..2fcf82c24 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithExamples.yaml @@ -0,0 +1,12 @@ +name: username +in: null +description: username to fetch +required: true +examples: + example1: + value: 5 + example2: + value: 7.5 +schema: + type: number + format: float \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml new file mode 100644 index 000000000..ab428863b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNoLocation.yaml @@ -0,0 +1,5 @@ +name: username +description: username to fetch +required: true +schema: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml new file mode 100644 index 000000000..0da935fbc --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithNullLocation.yaml @@ -0,0 +1,6 @@ +name: username +in: null +description: username to fetch +required: true +schema: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml new file mode 100644 index 000000000..0da935fbc --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/parameterWithUnknownLocation.yaml @@ -0,0 +1,6 @@ +name: username +in: null +description: username to fetch +required: true +schema: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/pathParameter.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/pathParameter.yaml new file mode 100644 index 000000000..e6e31c306 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/pathParameter.yaml @@ -0,0 +1,7 @@ +# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-object-examples +name: username +in: path +description: username to fetch +required: true +schema: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameter.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameter.yaml new file mode 100644 index 000000000..9d2ee3e1b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameter.yaml @@ -0,0 +1,11 @@ +# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-object-examples +name: id +in: query +description: ID of the object to fetch +required: false +schema: + type: array + items: + type: string +style: form +explode: true \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectType.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectType.yaml new file mode 100644 index 000000000..8aea05067 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectType.yaml @@ -0,0 +1,8 @@ +# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-object-examples +in: query +name: freeForm +schema: + type: object + additionalProperties: + type: integer +style: form \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectTypeAndContent.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectTypeAndContent.yaml new file mode 100644 index 000000000..89d3a84e0 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiParameter/queryParameterWithObjectTypeAndContent.yaml @@ -0,0 +1,15 @@ +# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameter-object-examples +in: query +name: coordinates +content: + application/json: + schema: + type: object + required: + - lat + - long + properties: + lat: + type: number + long: + type: number \ No newline at end of file diff --git a/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs b/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs index 6dde6905f..e424512ac 100644 --- a/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs +++ b/test/Microsoft.OpenApi.SmokeTests/ApiGurus.cs @@ -19,7 +19,7 @@ public class ApisGuruTests { private static HttpClient _httpClient; private readonly ITestOutputHelper _output; - + public ApisGuruTests(ITestOutputHelper output) { _output = output; @@ -42,22 +42,22 @@ public static IEnumerable GetSchemas() .GetStringAsync("https://api.apis.guru/v2/list.json") .GetAwaiter().GetResult(); - var json = JObject.Parse(listJsonStr); - foreach (var item in json.Properties()) + var json = JObject.Parse(listJsonStr); + foreach (var item in json.Properties()) + { + var versions = GetProp(item.Value, "versions") as JObject; + if (versions == null) + continue; + foreach (var prop in versions.Properties()) { - var versions = GetProp(item.Value, "versions") as JObject; - if (versions == null) - continue; - foreach (var prop in versions.Properties()) - { - var urlToJson = GetProp(prop.Value, "swaggerUrl")?.ToObject(); - if (urlToJson != null) - yield return new object[] { urlToJson }; + var urlToJson = GetProp(prop.Value, "swaggerUrl")?.ToObject(); + if (urlToJson != null) + yield return new object[] { urlToJson }; - var utlToYaml = GetProp(prop.Value, "swaggerYamlUrl")?.ToObject(); - if (utlToYaml != null) - yield return new object[] { utlToYaml }; - } + var utlToYaml = GetProp(prop.Value, "swaggerYamlUrl")?.ToObject(); + if (utlToYaml != null) + yield return new object[] { utlToYaml }; + } } JToken GetProp(JToken obj, string prop) @@ -70,10 +70,10 @@ JToken GetProp(JToken obj, string prop) } } - // [Theory(DisplayName = "APIs.guru")] - // [MemberData(nameof(GetSchemas))] + // Disable as some APIs are currently invalid [Theory(DisplayName = "APIs.guru")] + // [MemberData(nameof(GetSchemas))] public async Task EnsureThatICouldParse(string url) - { + { var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { @@ -94,12 +94,12 @@ public async Task EnsureThatICouldParse(string url) { _output.WriteLine($"Errors parsing {url}"); _output.WriteLine(String.Join("\n", diagnostic.Errors)); - // Assert.True(false); // Uncomment to identify descriptions with errors. + // Assert.True(false); // Uncomment to identify descriptions with errors. } Assert.NotNull(openApiDocument); stopwatch.Stop(); - _output.WriteLine($"Parsing {url} took {stopwatch.ElapsedMilliseconds} ms."); + _output.WriteLine($"Parsing {url} took {stopwatch.ElapsedMilliseconds} ms."); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/DefaultSettingsFixture.cs b/test/Microsoft.OpenApi.Tests/DefaultSettingsFixture.cs index f14a2ec14..9bb685ddc 100644 --- a/test/Microsoft.OpenApi.Tests/DefaultSettingsFixture.cs +++ b/test/Microsoft.OpenApi.Tests/DefaultSettingsFixture.cs @@ -19,7 +19,10 @@ public DefaultSettingsFixture() // given that there are multiple types that can be used for the declared type OpenApiAny. // Without this option, properties specific to those types would not be compared. AssertionOptions.AssertEquivalencyUsing( - o => o.AllowingInfiniteRecursion().RespectingRuntimeTypes()); + o => o + .AllowingInfiniteRecursion() + .RespectingRuntimeTypes() + .WithStrictOrdering()); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/DefaultSettingsFixtureCollection.cs b/test/Microsoft.OpenApi.Tests/DefaultSettingsFixtureCollection.cs index 21180a7c5..4fdfe0b16 100644 --- a/test/Microsoft.OpenApi.Tests/DefaultSettingsFixtureCollection.cs +++ b/test/Microsoft.OpenApi.Tests/DefaultSettingsFixtureCollection.cs @@ -16,4 +16,4 @@ namespace Microsoft.OpenApi.Tests public class DefaultSettingsFixtureCollection : ICollectionFixture { } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Expressions/MethodExpressionTests.cs b/test/Microsoft.OpenApi.Tests/Expressions/MethodExpressionTests.cs index 2c2b30943..1e13b17a5 100644 --- a/test/Microsoft.OpenApi.Tests/Expressions/MethodExpressionTests.cs +++ b/test/Microsoft.OpenApi.Tests/Expressions/MethodExpressionTests.cs @@ -13,7 +13,7 @@ public class MethodExpressionTests public void MethodExpressionReturnsCorrectExpression() { // Arrange & Act - var method = MethodExpression.Instance; + var method = new MethodExpression(); // Assert Assert.Equal("$method", method.Expression); diff --git a/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs b/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs index c583213bc..5c91249d3 100644 --- a/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs +++ b/test/Microsoft.OpenApi.Tests/Expressions/RuntimeExpressionTests.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using FluentAssertions; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Expressions; using Microsoft.OpenApi.Properties; using System; +using System.Collections.Generic; +using System.Linq; using Xunit; namespace Microsoft.OpenApi.Tests.Writers @@ -25,20 +28,7 @@ public void BuildRuntimeExpressionThrowsNullOrWhiteSpace(string expression) Assert.Throws("expression", test); } - [Theory] - [InlineData("method")] - [InlineData("abc")] - [InlineData("request")] - [InlineData("response")] - public void BuildRuntimeExpressionThrowsWithDollarPrefix(string expression) - { - // Arrange & Act - Action test = () => RuntimeExpression.Build(expression); - // Assert - OpenApiException exception = Assert.Throws(test); - Assert.Equal(String.Format(SRResource.RuntimeExpressionMustBeginWithDollar, expression), exception.Message); - } [Theory] [InlineData("$unknown")] @@ -145,31 +135,146 @@ public void BuildResponseRuntimeExpressionReturnsResponseExpression() } [Theory] + [InlineData("$response.body#/status")] + [InlineData("$request.header.accept")] [InlineData("$method")] [InlineData("$statusCode")] [InlineData("$url")] - public void CompareStaticRuntimeExpressionWorks(string expression) + public void BuildRuntimeExpressionTwiceCreatesNewEquivalentInstances(string expression) { // Arrange & Act var runtimeExpression1 = RuntimeExpression.Build(expression); var runtimeExpression2 = RuntimeExpression.Build(expression); // Assert - Assert.Same(runtimeExpression1, runtimeExpression2); + Assert.NotSame(runtimeExpression1, runtimeExpression2); + Assert.Equal(runtimeExpression1, runtimeExpression2); + } + + + [Fact] + public void CompositeRuntimeExpressionContainsExpression() + { + // Arrange + string expression = "This is a composite expression {$url} yay"; + + // Act + var runtimeExpression = RuntimeExpression.Build(expression); + + // Assert + Assert.NotNull(runtimeExpression); + var response = Assert.IsType(runtimeExpression); + Assert.Equal(expression, response.Expression); + + var compositeExpression = runtimeExpression as CompositeExpression; + Assert.Single(compositeExpression.ContainedExpressions); + + } + + [Fact] + public void CompositeRuntimeExpressionContainsMultipleExpressions() + { + // Arrange + string expression = "This is a composite expression {$url} yay and {$request.header.foo}"; + + // Act + var runtimeExpression = RuntimeExpression.Build(expression); + + // Assert + Assert.NotNull(runtimeExpression); + var response = Assert.IsType(runtimeExpression); + Assert.Equal(expression, response.Expression); + + var compositeExpression = runtimeExpression as CompositeExpression; + Assert.Equal(2, compositeExpression.ContainedExpressions.Count); + + compositeExpression.ContainedExpressions.Should().BeEquivalentTo(new List() + { + new UrlExpression(), + new RequestExpression(new HeaderExpression("foo")) + }); + } + + [Fact] + public void CompositeRuntimeExpressionForWebHook() + { + // Arrange + string expression = "http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}"; + + // Act + var runtimeExpression = RuntimeExpression.Build(expression); + + // Assert + Assert.NotNull(runtimeExpression); + var response = Assert.IsType(runtimeExpression); + Assert.Equal(expression, response.Expression); + + var compositeExpression = runtimeExpression as CompositeExpression; + Assert.Equal(2, compositeExpression.ContainedExpressions.Count); + + Assert.IsType(compositeExpression.ContainedExpressions.First()); + Assert.IsType(compositeExpression.ContainedExpressions.Last()); + } + + [Fact] + public void CompositeRuntimeExpressionWithMultipleRuntimeExpressionsAndFakeBraces() + { + // Arrange + string expression = "This is a composite expression {url} {test} and {} {$url} and {$request.header.foo}"; + + // Act + var runtimeExpression = RuntimeExpression.Build(expression); + + // Assert + runtimeExpression.Should().NotBeNull(); + runtimeExpression.Should().BeOfType(typeof(CompositeExpression)); + var response = (CompositeExpression)runtimeExpression; + response.Expression.Should().Be(expression); + + var compositeExpression = runtimeExpression as CompositeExpression; + compositeExpression.ContainedExpressions.Should().BeEquivalentTo(new List() + { + new UrlExpression(), + new RequestExpression(new HeaderExpression("foo")) + }); } [Theory] - [InlineData("$response.body#/status")] - [InlineData("$request.header.accept")] - public void CompareRuntimeExpressionWorks(string expression) + [InlineData("This is a composite expression {url} {test} and {$fakeRuntime} {$url} and {$request.header.foo}", "$fakeRuntime")] + [InlineData("This is a composite expression {url} {test} and {$} {$url} and {$request.header.foo}", "$")] + public void CompositeRuntimeExpressionWithInvalidRuntimeExpressions(string expression, string invalidExpression) { // Arrange & Act - var runtimeExpression1 = RuntimeExpression.Build(expression); - var runtimeExpression2 = RuntimeExpression.Build(expression); + Action test = () => RuntimeExpression.Build(expression); // Assert - Assert.NotSame(runtimeExpression1, runtimeExpression2); - Assert.Equal(runtimeExpression1, runtimeExpression2); + test.Should().Throw().WithMessage(String.Format(SRResource.RuntimeExpressionHasInvalidFormat, invalidExpression)); + } + + [Theory] + [InlineData("/simplePath")] + [InlineData("random string")] + [InlineData("method")] + [InlineData("/abc")] + [InlineData("request {}")] + [InlineData("response{}")] + public void CompositeRuntimeExpressionWithoutRecognizedRuntimeExpressions(string expression) + { + // Arrange + + // Act + var runtimeExpression = RuntimeExpression.Build(expression); + + // Assert + runtimeExpression.Should().NotBeNull(); + runtimeExpression.Should().BeOfType(typeof(CompositeExpression)); + var response = (CompositeExpression)runtimeExpression; + response.Expression.Should().Be(expression); + + var compositeExpression = runtimeExpression as CompositeExpression; + + // The whole string is treated as the template without any contained expressions. + compositeExpression.ContainedExpressions.Should().BeEmpty(); } } } diff --git a/test/Microsoft.OpenApi.Tests/Expressions/StatusCodeExpressionTests.cs b/test/Microsoft.OpenApi.Tests/Expressions/StatusCodeExpressionTests.cs index a60abd842..0433137da 100644 --- a/test/Microsoft.OpenApi.Tests/Expressions/StatusCodeExpressionTests.cs +++ b/test/Microsoft.OpenApi.Tests/Expressions/StatusCodeExpressionTests.cs @@ -13,7 +13,7 @@ public class StatusCodeExpressionTests public void StatusCodeExpressionReturnsCorrectExpression() { // Arrange & Act - var statusCode = StatusCodeExpression.Instance; + var statusCode = new StatusCodeExpression(); // Assert Assert.Equal("$statusCode", statusCode.Expression); diff --git a/test/Microsoft.OpenApi.Tests/Expressions/UrlExpressionTests.cs b/test/Microsoft.OpenApi.Tests/Expressions/UrlExpressionTests.cs index 69fdff8fa..291a02a4b 100644 --- a/test/Microsoft.OpenApi.Tests/Expressions/UrlExpressionTests.cs +++ b/test/Microsoft.OpenApi.Tests/Expressions/UrlExpressionTests.cs @@ -13,7 +13,7 @@ public class UrlExpressionTests public void UrlExpressionReturnsCorrectExpression() { // Arrange & Act - var url = UrlExpression.Instance; + var url = new UrlExpression(); // Assert Assert.Equal("$url", url.Expression); diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index 77c87aeed..bcf61dda2 100644 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -14,16 +14,18 @@ - - + + - + - + - + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs index 25d23cb85..fbc86e7f9 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiCallbackTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Expressions; @@ -106,7 +107,7 @@ public OpenApiCallbackTests(ITestOutputHelper output) public void SerializeAdvancedCallbackAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -145,7 +146,7 @@ public void SerializeAdvancedCallbackAsV3JsonWorks() public void SerializeReferencedCallbackAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -167,7 +168,7 @@ public void SerializeReferencedCallbackAsV3JsonWorks() public void SerializeReferencedCallbackAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -202,4 +203,4 @@ public void SerializeReferencedCallbackAsV3JsonWithoutReferenceWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs index b06866087..002143b15 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiComponentsTests.cs @@ -379,7 +379,7 @@ public void SerializeAdvancedComponentsWithReferenceAsJsonV3Works() // Act var actual = AdvancedComponentsWithReference.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -454,7 +454,7 @@ public void SerializeAdvancedComponentsWithReferenceAsYamlV3Works() // Act var actual = AdvancedComponentsWithReference.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -538,7 +538,7 @@ public void SerializeTopLevelReferencingComponentsAsYamlV3Works() // Act var actual = TopLevelReferencingComponents.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -579,11 +579,11 @@ public void SerializeTopLevelSelfReferencingWithOtherPropertiesComponentsAsYamlV // Act var actual = TopLevelSelfReferencingComponentsWithOtherProperties.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiContactTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiContactTests.cs index 1441be7c5..1a99241d1 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiContactTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiContactTests.cs @@ -91,4 +91,4 @@ public void SerializeAdvanceContactAsYamlWorks(OpenApiSpecVersion version) actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs index 6d24d71a8..ea65ec6eb 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; using Xunit; @@ -884,6 +886,95 @@ public class OpenApiDocumentTests Components = AdvancedComponents }; + public static OpenApiDocument DuplicateExtensions = new OpenApiDocument + { + Info = new OpenApiInfo + { + Version = "1.0.0", + Title = "Swagger Petstore (Simple)", + Description = "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + }, + Servers = new List + { + new OpenApiServer + { + Url = "http://petstore.swagger.io/api" + } + }, + Paths = new OpenApiPaths + { + ["/add/{operand1}/{operand2}"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + OperationId = "addByOperand1AndByOperand2", + Parameters = new List + { + new OpenApiParameter + { + Name = "operand1", + In = ParameterLocation.Path, + Description = "The first operand", + Required = true, + Schema = new OpenApiSchema + { + Type = "integer", + Extensions = new Dictionary + { + ["my-extension"] = new Any.OpenApiInteger(4), + } + }, + Extensions = new Dictionary + { + ["my-extension"] = new Any.OpenApiInteger(4), + } + }, + new OpenApiParameter + { + Name = "operand2", + In = ParameterLocation.Path, + Description = "The second operand", + Required = true, + Schema = new OpenApiSchema + { + Type = "integer", + Extensions = new Dictionary + { + ["my-extension"] = new Any.OpenApiInteger(4), + } + }, + Extensions = new Dictionary + { + ["my-extension"] = new Any.OpenApiInteger(4), + } + }, + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "pet response", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = PetSchema + } + }, + } + } + } + } + } + } + } + }; + private readonly ITestOutputHelper _output; public OpenApiDocumentTests(ITestOutputHelper output) @@ -895,7 +986,7 @@ public OpenApiDocumentTests(ITestOutputHelper output) public void SerializeAdvancedDocumentAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -1409,7 +1500,7 @@ public void SerializeAdvancedDocumentAsV3JsonWorks() public void SerializeAdvancedDocumentWithReferenceAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -1724,7 +1815,7 @@ public void SerializeAdvancedDocumentWithReferenceAsV3JsonWorks() public void SerializeAdvancedDocumentAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ ""swagger"": ""2.0"", @@ -2147,7 +2238,186 @@ public void SerializeAdvancedDocumentAsV2JsonWorks() AdvancedDocument.SerializeAsV2(writer); writer.Flush(); var actual = outputStringWriter.GetStringBuilder().ToString(); - + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeDuplicateExtensionsAsV3JsonWorks() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter); + var expected = @"{ + ""openapi"": ""3.0.1"", + ""info"": { + ""title"": ""Swagger Petstore (Simple)"", + ""description"": ""A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification"", + ""version"": ""1.0.0"" + }, + ""servers"": [ + { + ""url"": ""http://petstore.swagger.io/api"" + } + ], + ""paths"": { + ""/add/{operand1}/{operand2}"": { + ""get"": { + ""operationId"": ""addByOperand1AndByOperand2"", + ""parameters"": [ + { + ""name"": ""operand1"", + ""in"": ""path"", + ""description"": ""The first operand"", + ""required"": true, + ""schema"": { + ""type"": ""integer"", + ""my-extension"": 4 + }, + ""my-extension"": 4 + }, + { + ""name"": ""operand2"", + ""in"": ""path"", + ""description"": ""The second operand"", + ""required"": true, + ""schema"": { + ""type"": ""integer"", + ""my-extension"": 4 + }, + ""my-extension"": 4 + } + ], + ""responses"": { + ""200"": { + ""description"": ""pet response"", + ""content"": { + ""application/json"": { + ""schema"": { + ""type"": ""array"", + ""items"": { + ""required"": [ + ""id"", + ""name"" + ], + ""type"": ""object"", + ""properties"": { + ""id"": { + ""type"": ""integer"", + ""format"": ""int64"" + }, + ""name"": { + ""type"": ""string"" + }, + ""tag"": { + ""type"": ""string"" + } + } + } + } + } + } + } + } + } + } + } +}"; + + // Act + DuplicateExtensions.SerializeAsV3(writer); + writer.Flush(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeDuplicateExtensionsAsV2JsonWorks() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter); + var expected = @"{ + ""swagger"": ""2.0"", + ""info"": { + ""title"": ""Swagger Petstore (Simple)"", + ""description"": ""A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification"", + ""version"": ""1.0.0"" + }, + ""host"": ""petstore.swagger.io"", + ""basePath"": ""/api"", + ""schemes"": [ + ""http"" + ], + ""paths"": { + ""/add/{operand1}/{operand2}"": { + ""get"": { + ""operationId"": ""addByOperand1AndByOperand2"", + ""produces"": [ + ""application/json"" + ], + ""parameters"": [ + { + ""in"": ""path"", + ""name"": ""operand1"", + ""description"": ""The first operand"", + ""required"": true, + ""type"": ""integer"", + ""my-extension"": 4 + }, + { + ""in"": ""path"", + ""name"": ""operand2"", + ""description"": ""The second operand"", + ""required"": true, + ""type"": ""integer"", + ""my-extension"": 4 + } + ], + ""responses"": { + ""200"": { + ""description"": ""pet response"", + ""schema"": { + ""type"": ""array"", + ""items"": { + ""required"": [ + ""id"", + ""name"" + ], + ""type"": ""object"", + ""properties"": { + ""id"": { + ""format"": ""int64"", + ""type"": ""integer"" + }, + ""name"": { + ""type"": ""string"" + }, + ""tag"": { + ""type"": ""string"" + } + } + } + } + } + } + } + } + } +}"; + + // Act + DuplicateExtensions.SerializeAsV2(writer); + writer.Flush(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -2158,7 +2428,7 @@ public void SerializeAdvancedDocumentAsV2JsonWorks() public void SerializeAdvancedDocumentWithReferenceAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -2415,7 +2685,7 @@ public void SerializeAdvancedDocumentWithReferenceAsV2JsonWorks() AdvancedDocumentWithReference.SerializeAsV2(writer); writer.Flush(); var actual = outputStringWriter.GetStringBuilder().ToString(); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -2441,7 +2711,7 @@ public void SerializeSimpleDocumentWithTopLevelReferencingComponentsAsYamlV2Work // Act var actual = SimpleDocumentWithTopLevelReferencingComponents.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -2461,7 +2731,7 @@ public void SerializeSimpleDocumentWithTopLevelSelfReferencingComponentsAsYamlV3 // Act var actual = SimpleDocumentWithTopLevelSelfReferencingComponents.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -2490,11 +2760,158 @@ public void SerializeSimpleDocumentWithTopLevelSelfReferencingWithOtherPropertie // Act var actual = SimpleDocumentWithTopLevelSelfReferencingComponentsWithOtherProperties.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); actual.Should().Be(expected); } + + [Fact] + public void SerializeDocumentWithReferenceButNoComponents() + { + // Arrange + var document = new OpenApiDocument() + { + Info = new OpenApiInfo + { + Title = "Test", + Version = "1.0.0" + }, + Paths = new OpenApiPaths + { + ["/"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "test", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + } + } + } + }; + + + var reference = document.Paths["/"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema.Reference; + + // Act + var actual = document.Serialize(OpenApiSpecVersion.OpenApi2_0, OpenApiFormat.Json); + + // Assert + Assert.NotEmpty(actual); + } + + [Fact] + public void SerializeRelativePathAsV2JsonWorks() + { + // Arrange + var expected = + @"swagger: '2.0' +info: + version: 1.0.0 +basePath: /server1 +paths: { }"; + var doc = new OpenApiDocument() + { + Info = new OpenApiInfo() { Version = "1.0.0" }, + Servers = new List() { + new OpenApiServer() + { + Url = "/server1" + } + } + }; + + // Act + var actual = doc.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeRelativePathWithHostAsV2JsonWorks() + { + // Arrange + var expected = + @"swagger: '2.0' +info: + version: 1.0.0 +host: //example.org +basePath: /server1 +paths: { }"; + var doc = new OpenApiDocument() + { + Info = new OpenApiInfo() { Version = "1.0.0" }, + Servers = new List() { + new OpenApiServer() + { + Url = "//example.org/server1" + } + } + }; + + // Act + var actual = doc.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeRelativeRootPathWithHostAsV2JsonWorks() + { + // Arrange + var expected = + @"swagger: '2.0' +info: + version: 1.0.0 +host: //example.org +paths: { }"; + var doc = new OpenApiDocument() + { + Info = new OpenApiInfo() { Version = "1.0.0" }, + Servers = new List() { + new OpenApiServer() + { + Url = "//example.org/" + } + } + }; + + // Act + var actual = doc.SerializeAsYaml(OpenApiSpecVersion.OpenApi2_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiEncodingTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiEncodingTests.cs index cec2bc58a..24bfca242 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiEncodingTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiEncodingTests.cs @@ -75,4 +75,4 @@ public void SerializeAdvanceEncodingAsV3YamlWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs index f646b288a..896b96215 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiExampleTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Globalization; using System.IO; +using System.Text; using FluentAssertions; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; @@ -29,7 +31,9 @@ public class OpenApiExampleTests new OpenApiObject { ["href"] = new OpenApiString("http://example.com/1"), - ["rel"] = new OpenApiString("sampleRel1") + ["rel"] = new OpenApiString("sampleRel1"), + ["bytes"] = new OpenApiByte(new byte[] { 1, 2, 3 }), + ["binary"] = new OpenApiBinary(Encoding.UTF8.GetBytes("Ñ😻😑♮Í☛oƞ♑😲☇éNjžŁ♻😟¥a´Ī♃ƠąøƩ")) } } }, @@ -104,7 +108,7 @@ public OpenApiExampleTests(ITestOutputHelper output) public void SerializeAdvancedExampleAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -116,7 +120,9 @@ public void SerializeAdvancedExampleAsV3JsonWorks() ""links"": [ { ""href"": ""http://example.com/1"", - ""rel"": ""sampleRel1"" + ""rel"": ""sampleRel1"", + ""bytes"": ""AQID"", + ""binary"": ""Ñ😻😑♮Í☛oƞ♑😲☇éNjžŁ♻😟¥a´Ī♃ƠąøƩ"" } ] }, @@ -149,7 +155,7 @@ public void SerializeAdvancedExampleAsV3JsonWorks() public void SerializeReferencedExampleAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -171,7 +177,7 @@ public void SerializeReferencedExampleAsV3JsonWorks() public void SerializeReferencedExampleAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -212,4 +218,4 @@ public void SerializeReferencedExampleAsV3JsonWithoutReferenceWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiExternalDocsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiExternalDocsTests.cs index 62c93925a..7d37fc9a4 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiExternalDocsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiExternalDocsTests.cs @@ -78,4 +78,4 @@ public void SerializeAdvanceExDocsAsV3YamlWorks() #endregion } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs index 46a11cc54..5c2671e54 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiHeaderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Models; @@ -49,7 +50,7 @@ public OpenApiHeaderTests(ITestOutputHelper output) public void SerializeAdvancedHeaderAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -75,7 +76,7 @@ public void SerializeAdvancedHeaderAsV3JsonWorks() public void SerializeReferencedHeaderAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -97,7 +98,7 @@ public void SerializeReferencedHeaderAsV3JsonWorks() public void SerializeReferencedHeaderAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -123,7 +124,7 @@ public void SerializeReferencedHeaderAsV3JsonWithoutReferenceWorks() public void SerializeAdvancedHeaderAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -147,7 +148,7 @@ public void SerializeAdvancedHeaderAsV2JsonWorks() public void SerializeReferencedHeaderAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -169,7 +170,7 @@ public void SerializeReferencedHeaderAsV2JsonWorks() public void SerializeReferencedHeaderAsV2JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -189,4 +190,4 @@ public void SerializeReferencedHeaderAsV2JsonWithoutReferenceWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiInfoTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiInfoTests.cs index e2301b3f0..b2395a9ed 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiInfoTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiInfoTests.cs @@ -37,7 +37,7 @@ public class OpenApiInfoTests public static IEnumerable BasicInfoJsonExpected() { - var specVersions = new[] {OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0}; + var specVersions = new[] { OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0 }; foreach (var specVersion in specVersions) { yield return new object[] @@ -66,7 +66,7 @@ public void SerializeBasicInfoAsJsonWorks(OpenApiSpecVersion version, string exp public static IEnumerable BasicInfoYamlExpected() { - var specVersions = new[] {OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0}; + var specVersions = new[] { OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0 }; foreach (var specVersion in specVersions) { yield return new object[] @@ -93,7 +93,7 @@ public void SerializeBasicInfoAsYamlWorks(OpenApiSpecVersion version, string exp public static IEnumerable AdvanceInfoJsonExpect() { - var specVersions = new[] {OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0}; + var specVersions = new[] { OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0 }; foreach (var specVersion in specVersions) { yield return new object[] @@ -136,7 +136,7 @@ public void SerializeAdvanceInfoAsJsonWorks(OpenApiSpecVersion version, string e public static IEnumerable AdvanceInfoYamlExpect() { - var specVersions = new[] {OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0}; + var specVersions = new[] { OpenApiSpecVersion.OpenApi3_0, OpenApiSpecVersion.OpenApi2_0 }; foreach (var specVersion in specVersions) { yield return new object[] @@ -196,4 +196,4 @@ public void InfoVersionShouldAcceptDateStyledAsVersions() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiLicenseTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiLicenseTests.cs index 888d247fe..52e99b0b4 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiLicenseTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiLicenseTests.cs @@ -109,4 +109,4 @@ public void SerializeAdvanceLicenseAsYamlWorks(OpenApiSpecVersion version) actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs index 59c8904d4..ffcaa8804 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiLinkTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Any; @@ -79,7 +80,7 @@ public OpenApiLinkTests(ITestOutputHelper output) public void SerializeAdvancedLinkAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -111,7 +112,7 @@ public void SerializeAdvancedLinkAsV3JsonWorks() public void SerializeReferencedLinkAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -133,7 +134,7 @@ public void SerializeReferencedLinkAsV3JsonWorks() public void SerializeReferencedLinkAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -161,4 +162,4 @@ public void SerializeReferencedLinkAsV3JsonWithoutReferenceWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiMediaTypeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiMediaTypeTests.cs index d08dc13a9..c59da1e86 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiMediaTypeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiMediaTypeTests.cs @@ -7,6 +7,7 @@ using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Xunit; +using Xunit.Abstractions; namespace Microsoft.OpenApi.Tests.Models { @@ -24,6 +25,109 @@ public class OpenApiMediaTypeTests } }; + public static OpenApiMediaType MediaTypeWithObjectExample = new OpenApiMediaType + { + Example = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + + new OpenApiObject + { + ["status"] = new OpenApiString("Status2"), + ["id"] = new OpenApiString("v2"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/2"), + ["rel"] = new OpenApiString("sampleRel2") + } + } + } + } + }, + Encoding = new Dictionary + { + {"testEncoding", OpenApiEncodingTests.AdvanceEncoding} + } + }; + + public static OpenApiMediaType MediaTypeWithXmlExample = new OpenApiMediaType + { + Example = new OpenApiString("123"), + Encoding = new Dictionary + { + {"testEncoding", OpenApiEncodingTests.AdvanceEncoding} + } + }; + + public static OpenApiMediaType MediaTypeWithObjectExamples = new OpenApiMediaType + { + Examples = { + ["object1"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + + new OpenApiObject + { + ["status"] = new OpenApiString("Status2"), + ["id"] = new OpenApiString("v2"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/2"), + ["rel"] = new OpenApiString("sampleRel2") + } + } + } + } + } + } + }, + Encoding = new Dictionary + { + {"testEncoding", OpenApiEncodingTests.AdvanceEncoding} + } + }; + + private readonly ITestOutputHelper _output; + + public OpenApiMediaTypeTests(ITestOutputHelper output) + { + _output = output; + } + [Theory] [InlineData(OpenApiFormat.Json, "{ }")] [InlineData(OpenApiFormat.Yaml, "{ }")] @@ -85,5 +189,222 @@ public void SerializeAdvanceMediaTypeAsV3YamlWorks() expected = expected.MakeLineBreaksEnvironmentNeutral(); actual.Should().Be(expected); } + + [Fact] + public void SerializeMediaTypeWithObjectExampleAsV3YamlWorks() + { + // Arrange + var expected = + @"example: + versions: + - status: Status1 + id: v1 + links: + - href: http://example.com/1 + rel: sampleRel1 + - status: Status2 + id: v2 + links: + - href: http://example.com/2 + rel: sampleRel2 +encoding: + testEncoding: + contentType: 'image/png, image/jpeg' + style: simple + explode: true + allowReserved: true"; + + // Act + var actual = MediaTypeWithObjectExample.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeMediaTypeWithObjectExampleAsV3JsonWorks() + { + // Arrange + var expected = + @"{ + ""example"": { + ""versions"": [ + { + ""status"": ""Status1"", + ""id"": ""v1"", + ""links"": [ + { + ""href"": ""http://example.com/1"", + ""rel"": ""sampleRel1"" + } + ] + }, + { + ""status"": ""Status2"", + ""id"": ""v2"", + ""links"": [ + { + ""href"": ""http://example.com/2"", + ""rel"": ""sampleRel2"" + } + ] + } + ] + }, + ""encoding"": { + ""testEncoding"": { + ""contentType"": ""image/png, image/jpeg"", + ""style"": ""simple"", + ""explode"": true, + ""allowReserved"": true + } + } +}"; + + // Act + var actual = MediaTypeWithObjectExample.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeMediaTypeWithXmlExampleAsV3YamlWorks() + { + // Arrange + var expected = + @"example: 123 +encoding: + testEncoding: + contentType: 'image/png, image/jpeg' + style: simple + explode: true + allowReserved: true"; + + // Act + var actual = MediaTypeWithXmlExample.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeMediaTypeWithXmlExampleAsV3JsonWorks() + { + // Arrange + var expected = @"{ + ""example"": ""123"", + ""encoding"": { + ""testEncoding"": { + ""contentType"": ""image/png, image/jpeg"", + ""style"": ""simple"", + ""explode"": true, + ""allowReserved"": true + } + } +}"; + + // Act + var actual = MediaTypeWithXmlExample.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeMediaTypeWithObjectExamplesAsV3YamlWorks() + { + // Arrange + var expected = @"examples: + object1: + value: + versions: + - status: Status1 + id: v1 + links: + - href: http://example.com/1 + rel: sampleRel1 + - status: Status2 + id: v2 + links: + - href: http://example.com/2 + rel: sampleRel2 +encoding: + testEncoding: + contentType: 'image/png, image/jpeg' + style: simple + explode: true + allowReserved: true"; + + // Act + var actual = MediaTypeWithObjectExamples.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); + _output.WriteLine(actual); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeMediaTypeWithObjectExamplesAsV3JsonWorks() + { + // Arrange + var expected = @"{ + ""examples"": { + ""object1"": { + ""value"": { + ""versions"": [ + { + ""status"": ""Status1"", + ""id"": ""v1"", + ""links"": [ + { + ""href"": ""http://example.com/1"", + ""rel"": ""sampleRel1"" + } + ] + }, + { + ""status"": ""Status2"", + ""id"": ""v2"", + ""links"": [ + { + ""href"": ""http://example.com/2"", + ""rel"": ""sampleRel2"" + } + ] + } + ] + } + } + }, + ""encoding"": { + ""testEncoding"": { + ""contentType"": ""image/png, image/jpeg"", + ""style"": ""simple"", + ""explode"": true, + ""allowReserved"": true + } + } +}"; + + // Act + var actual = MediaTypeWithObjectExamples.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); + _output.WriteLine(actual); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowTests.cs index 0a8afe496..80040f566 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowTests.cs @@ -125,4 +125,4 @@ public void SerializeCompleteOAuthFlowAsV3JsonWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowsTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowsTests.cs index f90750d61..7d4882bbb 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOAuthFlowsTests.cs @@ -147,4 +147,4 @@ public void SerializeOAuthFlowsWithMultipleFlowsAsV3JsonWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs index 51ba8b08d..a59746214 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiOperationTests.cs @@ -359,6 +359,7 @@ public void SerializeOperationWithBodyAsV3JsonWorks() ""$ref"": ""#/components/responses/response1"" }, ""400"": { + ""description"": null, ""content"": { ""application/json"": { ""schema"": { @@ -431,6 +432,7 @@ public void SerializeAdvancedOperationWithTagAndSecurityAsV3JsonWorks() ""$ref"": ""#/components/responses/response1"" }, ""400"": { + ""description"": null, ""content"": { ""application/json"": { ""schema"": { @@ -554,7 +556,7 @@ public void SerializeOperationWithFormDataAsV3JsonWorks() // Act var actual = _operationWithFormData.SerializeAsJson(OpenApiSpecVersion.OpenApi3_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -607,7 +609,7 @@ public void SerializeOperationWithFormDataAsV2JsonWorks() // Act var actual = _operationWithFormData.SerializeAsJson(OpenApiSpecVersion.OpenApi2_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -658,6 +660,7 @@ public void SerializeOperationWithBodyAsV2JsonWorks() ""$ref"": ""#/responses/response1"" }, ""400"": { + ""description"": null, ""schema"": { ""maximum"": 10, ""minimum"": 5, @@ -672,7 +675,7 @@ public void SerializeOperationWithBodyAsV2JsonWorks() // Act var actual = _operationWithBody.SerializeAsJson(OpenApiSpecVersion.OpenApi2_0); - + // Assert actual = actual.MakeLineBreaksEnvironmentNeutral(); expected = expected.MakeLineBreaksEnvironmentNeutral(); @@ -727,6 +730,7 @@ public void SerializeAdvancedOperationWithTagAndSecurityAsV2JsonWorks() ""$ref"": ""#/responses/response1"" }, ""400"": { + ""description"": null, ""schema"": { ""maximum"": 10, ""minimum"": 5, @@ -779,4 +783,4 @@ public void SerializeOperationWithNullCollectionAsV2JsonWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs index d01f95b4b..f37d18ed7 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiParameterTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; @@ -47,7 +49,7 @@ public class OpenApiParameterTests Title = "title2", Description = "description2" }, - Examples = new Dictionary + Examples = new Dictionary { ["test"] = new OpenApiExample { @@ -57,6 +59,50 @@ public class OpenApiParameterTests } }; + public static OpenApiParameter ParameterWithFormStyleAndExplodeFalse = new OpenApiParameter + { + Name = "name1", + In = ParameterLocation.Query, + Description = "description1", + Style = ParameterStyle.Form, + Explode = false, + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Enum = new List + { + new OpenApiString("value1"), + new OpenApiString("value2") + } + } + } + + }; + + public static OpenApiParameter ParameterWithFormStyleAndExplodeTrue = new OpenApiParameter + { + Name = "name1", + In = ParameterLocation.Query, + Description = "description1", + Style = ParameterStyle.Form, + Explode = true, + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Enum = new List + { + new OpenApiString("value1"), + new OpenApiString("value2") + } + } + } + + }; + public static OpenApiParameter AdvancedHeaderParameterWithSchemaReference = new OpenApiParameter { Name = "name1", @@ -171,7 +217,7 @@ public void SerializeAdvancedParameterAsV3JsonWorks() public void SerializeReferencedParameterAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -193,7 +239,7 @@ public void SerializeReferencedParameterAsV3JsonWorks() public void SerializeReferencedParameterAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -216,7 +262,7 @@ public void SerializeReferencedParameterAsV3JsonWithoutReferenceWorks() public void SerializeReferencedParameterAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -238,7 +284,7 @@ public void SerializeReferencedParameterAsV2JsonWorks() public void SerializeReferencedParameterAsV2JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -261,7 +307,7 @@ public void SerializeReferencedParameterAsV2JsonWithoutReferenceWorks() public void SerializeParameterWithSchemaReferenceAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -287,7 +333,7 @@ public void SerializeParameterWithSchemaReferenceAsV2JsonWorks() public void SerializeParameterWithSchemaTypeObjectAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -308,5 +354,74 @@ public void SerializeParameterWithSchemaTypeObjectAsV2JsonWorks() expected = expected.MakeLineBreaksEnvironmentNeutral(); actual.Should().Be(expected); } + + [Fact] + public void SerializeParameterWithFormStyleAndExplodeFalseWorks() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter); + var expected = + @"{ + ""name"": ""name1"", + ""in"": ""query"", + ""description"": ""description1"", + ""style"": ""form"", + ""explode"": false, + ""schema"": { + ""type"": ""array"", + ""items"": { + ""enum"": [ + ""value1"", + ""value2"" + ] + } + } +}"; + + // Act + ParameterWithFormStyleAndExplodeFalse.SerializeAsV3WithoutReference(writer); + writer.Flush(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } + + [Fact] + public void SerializeParameterWithFormStyleAndExplodeTrueWorks() + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter); + var expected = + @"{ + ""name"": ""name1"", + ""in"": ""query"", + ""description"": ""description1"", + ""style"": ""form"", + ""schema"": { + ""type"": ""array"", + ""items"": { + ""enum"": [ + ""value1"", + ""value2"" + ] + } + } +}"; + + // Act + ParameterWithFormStyleAndExplodeTrue.SerializeAsV3WithoutReference(writer); + writer.Flush(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); + } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiReferenceTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiReferenceTests.cs index 9e5b347ab..c251814db 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiReferenceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiReferenceTests.cs @@ -65,7 +65,7 @@ public void SettingExternalReferenceShouldSucceed(string expected, string extern public void SerializeSchemaReferenceAsJsonV3Works() { // Arrange - var reference = new OpenApiReference {Type = ReferenceType.Schema, Id = "Pet"}; + var reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Pet" }; var expected = @"{ ""$ref"": ""#/components/schemas/Pet"" }"; @@ -182,7 +182,7 @@ public void SerializeExternalReferenceAsYamlV2Works() public void SerializeExternalReferenceAsJsonV3Works() { // Arrange - var reference = new OpenApiReference {ExternalResource = "main.json", Id = "Pets"}; + var reference = new OpenApiReference { ExternalResource = "main.json", Id = "Pets" }; var expected = @"{ ""$ref"": ""main.json#/Pets"" @@ -201,7 +201,7 @@ public void SerializeExternalReferenceAsJsonV3Works() public void SerializeExternalReferenceAsYamlV3Works() { // Arrange - var reference = new OpenApiReference {ExternalResource = "main.json", Id = "Pets"}; + var reference = new OpenApiReference { ExternalResource = "main.json", Id = "Pets" }; var expected = @"$ref: main.json#/Pets"; // Act @@ -211,4 +211,4 @@ public void SerializeExternalReferenceAsYamlV3Works() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs index a7c9c3641..b225417fc 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiRequestBodyTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Models; @@ -61,7 +62,7 @@ public OpenApiRequestBodyTests(ITestOutputHelper output) public void SerializeAdvancedRequestBodyAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -91,7 +92,7 @@ public void SerializeAdvancedRequestBodyAsV3JsonWorks() public void SerializeReferencedRequestBodyAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -113,7 +114,7 @@ public void SerializeReferencedRequestBodyAsV3JsonWorks() public void SerializeReferencedRequestBodyAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -139,4 +140,4 @@ public void SerializeReferencedRequestBodyAsV3JsonWithoutReferenceWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs index 8e3b91621..9b86a6d51 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiResponseTests.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; using Xunit; @@ -32,7 +35,11 @@ public class OpenApiResponseTests Reference = new OpenApiReference {Type = ReferenceType.Schema, Id = "customType"} } }, - Example = new OpenApiString("Blabla") + Example = new OpenApiString("Blabla"), + Extensions = new Dictionary + { + ["myextension"] = new OpenApiString("myextensionvalue"), + }, } }, Headers = @@ -115,11 +122,18 @@ public void SerializeBasicResponseWorks( OpenApiSpecVersion version, OpenApiFormat format) { - // Arrange & Act + // Arrange + var expected = format == OpenApiFormat.Json ? @"{ + ""description"": null +}" : @"description: "; + + // Act var actual = BasicResponse.Serialize(version, format); // Assert - actual.Should().Be("{ }"); + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + actual.Should().Be(expected); } [Fact] @@ -150,7 +164,8 @@ public void SerializeAdvancedResponseAsV3JsonWorks() ""$ref"": ""#/components/schemas/customType"" } }, - ""example"": ""Blabla"" + ""example"": ""Blabla"", + ""myextension"": ""myextensionvalue"" } } }"; @@ -185,7 +200,8 @@ public void SerializeAdvancedResponseAsV3YamlWorks() type: array items: $ref: '#/components/schemas/customType' - example: Blabla"; + example: Blabla + myextension: myextensionvalue"; // Act var actual = AdvancedResponse.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); @@ -211,6 +227,7 @@ public void SerializeAdvancedResponseAsV2JsonWorks() ""examples"": { ""text/plain"": ""Blabla"" }, + ""myextension"": ""myextensionvalue"", ""headers"": { ""X-Rate-Limit-Limit"": { ""description"": ""The number of allowed requests in the current period"", @@ -244,6 +261,7 @@ public void SerializeAdvancedResponseAsV2YamlWorks() $ref: '#/definitions/customType' examples: text/plain: Blabla +myextension: myextensionvalue headers: X-Rate-Limit-Limit: description: The number of allowed requests in the current period @@ -265,7 +283,7 @@ public void SerializeAdvancedResponseAsV2YamlWorks() public void SerializeReferencedResponseAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -287,7 +305,7 @@ public void SerializeReferencedResponseAsV3JsonWorks() public void SerializeReferencedResponseAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -333,7 +351,7 @@ public void SerializeReferencedResponseAsV3JsonWithoutReferenceWorks() public void SerializeReferencedResponseAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -355,7 +373,7 @@ public void SerializeReferencedResponseAsV2JsonWorks() public void SerializeReferencedResponseAsV2JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -389,4 +407,4 @@ public void SerializeReferencedResponseAsV2JsonWithoutReferenceWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs index 5625f9f87..4f9510132 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSchemaTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Any; @@ -163,7 +164,7 @@ public class OpenApiSchemaTests public static OpenApiSchema AdvancedSchemaWithRequiredPropertiesObject = new OpenApiSchema { Title = "title1", - Required = new HashSet(){ "property1" }, + Required = new HashSet() { "property1" }, Properties = new Dictionary { ["property1"] = new OpenApiSchema @@ -368,7 +369,7 @@ public void SerializeAdvancedSchemaWithAllOfAsV3JsonWorks() public void SerializeReferencedSchemaAsV3WithoutReferenceJsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -400,7 +401,7 @@ public void SerializeReferencedSchemaAsV3WithoutReferenceJsonWorks() public void SerializeReferencedSchemaAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -422,7 +423,7 @@ public void SerializeReferencedSchemaAsV3JsonWorks() public void SerializeSchemaWRequiredPropertiesAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ ""title"": ""title1"", @@ -477,4 +478,4 @@ public void SerializeSchemaWRequiredPropertiesAsV2JsonWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecurityRequirementTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecurityRequirementTests.cs index 134526ebb..7d630c5f6 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecurityRequirementTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecurityRequirementTests.cs @@ -21,7 +21,7 @@ public class OpenApiSecurityRequirementTests [ new OpenApiSecurityScheme { - Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme1"} + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "scheme1" } } ] = new List { @@ -32,7 +32,7 @@ public class OpenApiSecurityRequirementTests [ new OpenApiSecurityScheme { - Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme2"} + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "scheme2" } } ] = new List { @@ -42,7 +42,7 @@ public class OpenApiSecurityRequirementTests [ new OpenApiSecurityScheme { - Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme3"} + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "scheme3" } } ] = new List() }; @@ -53,7 +53,7 @@ public class OpenApiSecurityRequirementTests [ new OpenApiSecurityScheme { - Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme1"} + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "scheme1" } } ] = new List { @@ -75,7 +75,7 @@ public class OpenApiSecurityRequirementTests [ new OpenApiSecurityScheme { - Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme3"} + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "scheme3" } } ] = new List() }; @@ -255,7 +255,7 @@ public void SchemesShouldConsiderOnlyReferenceIdForEquality() // Act securityRequirement.Add(securityScheme1, new List()); - securityRequirement.Add(securityScheme2, new List {"scope1", "scope2"}); + securityRequirement.Add(securityScheme2, new List { "scope1", "scope2" }); Action addSecurityScheme1Duplicate = () => securityRequirement.Add(securityScheme1Duplicate, new List()); @@ -265,19 +265,19 @@ public void SchemesShouldConsiderOnlyReferenceIdForEquality() // Assert // Only the first two should be added successfully since the latter two are duplicates of securityScheme1. // Duplicate determination only considers Reference.Id. - addSecurityScheme1Duplicate.ShouldThrow(); - addSecurityScheme1WithDifferentProperties.ShouldThrow(); + addSecurityScheme1Duplicate.Should().Throw(); + addSecurityScheme1WithDifferentProperties.Should().Throw(); securityRequirement.Should().HaveCount(2); - securityRequirement.ShouldBeEquivalentTo( + securityRequirement.Should().BeEquivalentTo( new OpenApiSecurityRequirement { // This should work with any security scheme object // as long as Reference.Id os securityScheme1 [securityScheme1WithDifferentProperties] = new List(), - [securityScheme2] = new List {"scope1", "scope2"}, + [securityScheme2] = new List { "scope1", "scope2" }, }); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs index d421b1b2b..5fb99cb95 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiSecuritySchemeTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Extensions; @@ -300,7 +301,7 @@ public void SerializeOpenIdConnectSecuritySchemeAsV3JsonWorks() public void SerializeReferencedSecuritySchemeAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -327,7 +328,7 @@ public void SerializeReferencedSecuritySchemeAsV3JsonWorks() public void SerializeReferencedSecuritySchemeAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -347,4 +348,4 @@ public void SerializeReferencedSecuritySchemeAsV3JsonWithoutReferenceWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiServerTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiServerTests.cs index 5557d7e0e..e4d4f36fb 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiServerTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiServerTests.cs @@ -101,4 +101,4 @@ public void SerializeAdvancedServerAsV3JsonWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiServerVariableTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiServerVariableTests.cs index 613c95971..9d50f76f6 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiServerVariableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiServerVariableTests.cs @@ -82,4 +82,4 @@ public void SerializeAdvancedServerVariableAsV3YamlWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs index 8a74181ae..4920e165d 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiTagTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Any; @@ -48,7 +49,7 @@ public class OpenApiTagTests public void SerializeBasicTagAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = "{ }"; @@ -67,7 +68,7 @@ public void SerializeBasicTagAsV3JsonWithoutReferenceWorks() public void SerializeBasicTagAsV2JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = "{ }"; @@ -86,7 +87,7 @@ public void SerializeBasicTagAsV2JsonWithoutReferenceWorks() public void SerializeBasicTagAsV3YamlWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = "{ }"; @@ -104,7 +105,7 @@ public void SerializeBasicTagAsV3YamlWithoutReferenceWorks() public void SerializeBasicTagAsV2YamlWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = "{ }"; @@ -123,7 +124,7 @@ public void SerializeBasicTagAsV2YamlWithoutReferenceWorks() public void SerializeAdvancedTagAsV3JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -151,7 +152,7 @@ public void SerializeAdvancedTagAsV3JsonWithoutReferenceWorks() public void SerializeAdvancedTagAsV2JsonWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"{ @@ -179,7 +180,7 @@ public void SerializeAdvancedTagAsV2JsonWithoutReferenceWorks() public void SerializeAdvancedTagAsV3YamlWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = @"name: pet @@ -204,7 +205,7 @@ public void SerializeAdvancedTagAsV3YamlWithoutReferenceWorks() public void SerializeAdvancedTagAsV2YamlWithoutReferenceWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = @"name: pet @@ -229,7 +230,7 @@ public void SerializeAdvancedTagAsV2YamlWithoutReferenceWorks() public void SerializeAdvancedTagAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"""pet"""; @@ -249,7 +250,7 @@ public void SerializeAdvancedTagAsV3JsonWorks() public void SerializeAdvancedTagAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"""pet"""; @@ -269,7 +270,7 @@ public void SerializeAdvancedTagAsV2JsonWorks() public void SerializeAdvancedTagAsV3YamlWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = @" pet"; @@ -289,7 +290,7 @@ public void SerializeAdvancedTagAsV3YamlWorks() public void SerializeAdvancedTagAsV2YamlWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = @" pet"; @@ -309,7 +310,7 @@ public void SerializeAdvancedTagAsV2YamlWorks() public void SerializeReferencedTagAsV3JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"""pet"""; @@ -329,7 +330,7 @@ public void SerializeReferencedTagAsV3JsonWorks() public void SerializeReferencedTagAsV2JsonWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); var expected = @"""pet"""; @@ -349,7 +350,7 @@ public void SerializeReferencedTagAsV2JsonWorks() public void SerializeReferencedTagAsV3YamlWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = @" pet"; @@ -369,7 +370,7 @@ public void SerializeReferencedTagAsV3YamlWorks() public void SerializeReferencedTagAsV2YamlWorks() { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); var expected = @" pet"; @@ -385,4 +386,4 @@ public void SerializeReferencedTagAsV2YamlWorks() actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs b/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs index 77c834042..9e79c5211 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiXmlTests.cs @@ -95,4 +95,4 @@ public void SerializeAdvancedXmlAsYamlWorks(OpenApiSpecVersion version) actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTestCases.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTestCases.cs new file mode 100644 index 000000000..69ab1d1ae --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTestCases.cs @@ -0,0 +1,2407 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; + +namespace Microsoft.OpenApi.Tests.Services +{ + internal static class OpenApiComparerTestCases + { + public static IEnumerable GetTestCasesForOpenApiComparerShouldSucceed() + { + // New and removed paths + yield return new object[] + { + "New And Removed Paths", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/newPath", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths/~1newPath", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiPathItem), + SourceValue = null, + TargetValue = new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiPathItem), + TargetValue = null, + SourceValue = new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + } + } + }; + + // New and removed operations + yield return new object[] + { + "New And Removed Operations", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Post, new OpenApiOperation() + } + } + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Patch, new OpenApiOperation() + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths/~1test/patch", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiOperation), + SourceValue = null, + TargetValue = new OpenApiOperation() + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/post", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiOperation), + TargetValue = null, + SourceValue = new OpenApiOperation() + } + } + }; + + // Empty target document paths + yield return new object[] + { + "Empty target document", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + } + } + }, + new OpenApiDocument(), + new List + { + new OpenApiDifference + { + Pointer = "#/paths", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiPaths), + SourceValue = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + } + }, + TargetValue = null + } + } + }; + + // Empty source document + yield return new object[] + { + "Empty source document", + new OpenApiDocument(), + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/newPath", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiPaths), + SourceValue = null, + TargetValue = new OpenApiPaths + { + { + "/newPath", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + } + } + } + } + } + } + } + }; + + // Empty target operations + yield return new object[] + { + "Empty target operations", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Post, new OpenApiOperation() + } + } + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary() + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths/~1test/get", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiOperation), + TargetValue = null, + SourceValue = new OpenApiOperation() + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/post", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiOperation), + TargetValue = null, + SourceValue = new OpenApiOperation() + } + } + }; + + // Empty source operations + yield return new object[] + { + "Empty source operations", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary() + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Patch, new OpenApiOperation() + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths/~1test/get", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiOperation), + SourceValue = null, + TargetValue = new OpenApiOperation() + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/patch", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiOperation), + SourceValue = null, + TargetValue = new OpenApiOperation() + } + } + }; + + // Identical source and target + yield return new object[] + { + "Identical source and target documents", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Post, new OpenApiOperation() + } + } + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Post, new OpenApiOperation() + } + } + } + } + } + }, + new List() + }; + + // Differences in summary and description + yield return new object[] + { + "Differences in summary and description", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Post, new OpenApiOperation() + } + } + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "updated", + Description = "updated", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation() + }, + { + OperationType.Post, new OpenApiOperation() + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths/~1test/summary", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "test", + TargetValue = "updated" + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "test", + TargetValue = "updated" + } + } + }; + + // Differences in schema + yield return new object[] + { + "Differences in schema", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation + { + Parameters = new List + { + new OpenApiParameter + { + Name = "Test Parameter", + In = ParameterLocation.Path, + Schema = new OpenApiSchema + { + Title = "title1", + MultipleOf = 3, + Maximum = 42, + ExclusiveMinimum = true, + Minimum = 10, + Default = new OpenApiInteger(15), + Type = "integer", + + Nullable = true, + ExternalDocs = new OpenApiExternalDocs + { + Url = new Uri("http://example.com/externalDocs") + }, + + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + } + } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + }, + Example = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation + { + Parameters = new List + { + new OpenApiParameter + { + Name = "Test Parameter", + In = ParameterLocation.Path, + Schema = new OpenApiSchema + { + Title = "title1", + MultipleOf = 3, + Maximum = 42, + ExclusiveMinimum = true, + Minimum = 10, + Default = new OpenApiInteger(15), + Type = "integer", + + Nullable = true, + ExternalDocs = new OpenApiExternalDocs + { + Url = new Uri("http://example.com/externalDocs") + }, + + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + } + } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + }, + Example = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/parameters/0/schema/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/parameters/0/schema/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/parameters/0/schema/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/parameters/0/schema/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/components/schemas/schemaObject1/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/components/schemas/schemaObject1/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject1/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject1/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject2/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject2/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/components/schemas/schemaObject1/example", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + TargetValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/components/schemas/schemaObject2/properties/property6/example", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + TargetValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/parameters/0/schema/properties/property6/properties/property6/example", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + TargetValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/parameters/0/schema/example", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + TargetValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject1/properties/property6/properties/property6/example", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + TargetValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }; + + // Differences in request and response + yield return new object[] + { + "Differences in request and response", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + }, + Examples = new Dictionary + { + { + "example1", new OpenApiExample + { + Reference = new OpenApiReference + { + Id = "example1", + Type = ReferenceType.Example + } + } + } + } + } + } + }, + Responses = new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + } + } + } + } + } + } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + Examples = new Dictionary + { + ["example1"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }, + ["example3"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + }, + Examples = new Dictionary + { + { + "example1", new OpenApiExample + { + Reference = new OpenApiReference + { + Id = "example1", + Type = ReferenceType.Example + } + } + } + } + } + } + }, + Responses = new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + }, + { + "400", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + } + } + } + } + } + } + } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + Examples = new Dictionary + { + ["example1"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }, + ["example3"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/requestBody/content/application~1xml/schema/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/requestBody/content/application~1xml/schema/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/requestBody/content/application~1xml/schema/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/requestBody/content/application~1xml/schema/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/responses/400", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiResponse), + SourceValue = null, + TargetValue = new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/responses/200/content/application~1json/schema/items/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/responses/200/content/application~1json/schema/items/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/responses/200/content/application~1json/schema/items/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/responses/200/content/application~1json/schema/items/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/components/schemas/schemaObject1/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/components/schemas/schemaObject1/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject1/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject1/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject2/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/components/schemas/schemaObject2/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/requestBody/content/application~1xml/examples/example1/value", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + }, + TargetValue = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/components/examples/example1/value", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + }, + TargetValue = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relupdate"] = new OpenApiString("sampleRel1") + } + } + } + } + } + } + } + }; + + // Differences in tags and security requirements + yield return new object[] + { + "Differences in tags and security requirements", + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + Responses = new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + Security = new List + { + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme1" + } + } + ] = new List() + } + } + } + } + } + } + } + }, + Tags = new List + { + new OpenApiTag + { + Description = "test description", + Name = "Tag1", + ExternalDocs = new OpenApiExternalDocs + { + Description = "test description", + Url = new Uri("http://localhost/doc") + } + }, + new OpenApiTag + { + Description = "test description", + Name = "Tag2", + ExternalDocs = new OpenApiExternalDocs + { + Description = "test description", + Url = new Uri("http://localhost/doc") + } + } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + SecuritySchemes = new Dictionary + { + { + "scheme1", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test", + Flows = new OpenApiOAuthFlows + { + Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/1") + }, + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + } + } + }, + { + "scheme2", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + { + "scheme3", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + } + } + }, + SecurityRequirements = new List + { + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme1" + } + } + ] = new List() + }, + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme2" + } + } + ] = new List() + }, + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme3" + } + } + ] = new List() + } + } + }, + new OpenApiDocument + { + Paths = new OpenApiPaths + { + { + "/test", new OpenApiPathItem + { + Summary = "test", + Description = "test", + Operations = new Dictionary + { + { + OperationType.Get, new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + Responses = new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + Security = new List + { + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme1" + } + }, + new List() + } + }, + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme4" + } + } + ] = new List() + } + } + } + } + } + } + } + }, + Tags = new List + { + new OpenApiTag + { + Description = "test description updated", + Name = "Tag1", + ExternalDocs = new OpenApiExternalDocs + { + Description = "test description", + Url = new Uri("http://localhost/doc") + } + } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + SecuritySchemes = new Dictionary + { + { + "scheme1", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test", + Flows = new OpenApiOAuthFlows + { + Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/3") + }, + ClientCredentials = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + } + } + }, + { + "scheme2", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + { + "scheme4", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + } + } + }, + SecurityRequirements = new List + { + new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme1" + } + }, + new List() + }, + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme2" + } + }, + new List() + } + }, + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme4" + } + } + ] = new List() + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/security/0/scheme2", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(IList), + SourceValue = null, + TargetValue = new List() + }, + new OpenApiDifference + { + Pointer = "#/security/1/scheme4", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(IList), + SourceValue = null, + TargetValue = new List() + }, + new OpenApiDifference + { + Pointer = + "#/components/securitySchemes/scheme4", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSecurityScheme), + SourceValue = null, + TargetValue = new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/security/1", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSecurityRequirement), + SourceValue = null, + TargetValue = new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme4" + } + }, + new List() + } + } + }, + new OpenApiDifference + { + Pointer = "#/tags/0/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "test description", + TargetValue = "test description updated" + }, + new OpenApiDifference + { + Pointer = "#/components/securitySchemes/scheme1/flows/implicit/authorizationUrl", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + SourceValue = "http://localhost/1", + TargetValue = "http://localhost/3" + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/security/0/scheme1/flows/implicit/authorizationUrl", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + SourceValue = "http://localhost/1", + TargetValue = "http://localhost/3" + }, + new OpenApiDifference + { + Pointer = "#/security/0/scheme1/flows/implicit/authorizationUrl", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + SourceValue = "http://localhost/1", + TargetValue = "http://localhost/3" + }, + new OpenApiDifference + { + Pointer = + "#/components/securitySchemes/scheme1/flows/clientCredentials", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = null, + TargetValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + }, + new OpenApiDifference + { + Pointer = + "#/paths/~1test/get/security/0/scheme1/flows/clientCredentials", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = null, + TargetValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + }, + new OpenApiDifference + { + Pointer = + "#/security/0/scheme1/flows/clientCredentials", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = null, + TargetValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + }, + new OpenApiDifference + { + Pointer = "#/components/securitySchemes/scheme1/flows/authorizationCode", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/paths/~1test/get/security/0/scheme1/flows/authorizationCode", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/security/0/scheme1/flows/authorizationCode", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/components/securitySchemes/scheme3", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSecurityScheme), + SourceValue = new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/tags/1", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiTag), + SourceValue = new OpenApiTag + { + Description = "test description", + Name = "Tag2", + ExternalDocs = new OpenApiExternalDocs + { + Description = "test description", + Url = new Uri("http://localhost/doc") + } + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/security/2", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSecurityRequirement), + SourceValue = new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme3" + } + } + ] = new List() + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/security/1/scheme2", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(IList), + SourceValue = new List(), + TargetValue = null + } + } + }; + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTests.cs new file mode 100644 index 000000000..a100517f0 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiComparerTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiComparerTests + { + public static OpenApiExample AdvancedExample = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + }, + + new OpenApiObject + { + ["status"] = new OpenApiString("Status2"), + ["id"] = new OpenApiString("v2"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/2"), + ["rel"] = new OpenApiString("sampleRel2") + } + } + } + } + } + }; + + private readonly ITestOutputHelper _output; + + public OpenApiComparerTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory(Skip = "Need to fix")] + [MemberData( + nameof(OpenApiComparerTestCases.GetTestCasesForOpenApiComparerShouldSucceed), + MemberType = typeof(OpenApiComparerTestCases))] + public void OpenApiComparerShouldSucceed( + string testCaseName, + OpenApiDocument source, + OpenApiDocument target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + new OpenApiExampleComparer().Compare(AdvancedExample, AdvancedExample, + new ComparisonContext(new OpenApiComparerFactory(), new OpenApiDocument(), new OpenApiDocument())); + var differences = OpenApiComparer.Compare(source, target).ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiComponentsTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiComponentsTests.cs new file mode 100644 index 000000000..221661bff --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiComponentsTests.cs @@ -0,0 +1,908 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiComponentsTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiComponentsTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiComponentsComparerShouldSucceed() + { + // Differences in schema and request body + yield return new object[] + { + "Differences in schema and request body", + new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + }, + new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/requestBodies/requestBody1/content/application~1json/schema/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/requestBodies/requestBody1/content/application~1json/schema/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/requestBodies/requestBody1/content/application~1json/schema/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/requestBodies/requestBody1/content/application~1json/schema/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/schemas/schemaObject1/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/schemas/schemaObject1/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/schemas/schemaObject1/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/schemas/schemaObject1/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/schemas/schemaObject2/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/schemas/schemaObject2/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/schemas/schemaObject2/properties/property6/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/schemas/schemaObject2/properties/property6/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + } + } + }; + + // New schema and request body + yield return new object[] + { + "New schema and request body", + new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + } + }, + new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/requestBodies/requestBody2", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiRequestBody), + SourceValue = null, + TargetValue = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + }, + new OpenApiDifference + { + Pointer = + "#/schemas/schemaObject2", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + } + } + } + } + } + }; + + // New, removed and updated examples + yield return new object[] + { + "New, removed and updated examples", + new OpenApiComponents + { + Examples = new Dictionary + { + ["example1"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }, + ["example3"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + } + }, + new OpenApiComponents + { + Examples = new Dictionary + { + ["example2"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }, + ["example3"] = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/examples/example2", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiExample), + SourceValue = null, + TargetValue = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/examples/example1", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiExample), + SourceValue = new OpenApiExample + { + Value = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/examples/example3/value", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + SourceValue = new OpenApiObject + { + ["versions"] = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + }, + TargetValue = new OpenApiObject + { + ["versions"] = new OpenApiArray + { + new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + } + } + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiComponentsComparerShouldSucceed))] + public void OpenApiComponentsComparerShouldSucceed( + string testCaseName, + OpenApiComponents source, + OpenApiComponents target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiComponentsComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiEncodingComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiEncodingComparerTests.cs new file mode 100644 index 000000000..2982f40de --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiEncodingComparerTests.cs @@ -0,0 +1,360 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiEncodingComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiEncodingComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiEncodingComparerShouldSucceed() + { + // Differences in ContentType,Style,Explode and AllowReserved + yield return new object[] + { + "Differences in ContentType,Style,Explode and AllowReserved", + new OpenApiEncoding + { + ContentType = "image/png, image/jpeg", + Style = ParameterStyle.Simple, + Explode = true, + AllowReserved = true + }, + new OpenApiEncoding + { + ContentType = "image/jpeg", + Style = ParameterStyle.Form, + Explode = false, + AllowReserved = false + }, + new List + { + new OpenApiDifference + { + Pointer = "#/contentType", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + TargetValue = "image/jpeg", + SourceValue = "image/png, image/jpeg" + }, + new OpenApiDifference + { + Pointer = "#/style", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(ParameterStyle), + TargetValue = ParameterStyle.Form, + SourceValue = ParameterStyle.Simple + }, + new OpenApiDifference + { + Pointer = "#/explode", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = false, + SourceValue = true + }, + new OpenApiDifference + { + Pointer = "#/allowReserved", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = false, + SourceValue = true + } + } + }; + + // Null source + yield return new object[] + { + "Null source", + null, + new OpenApiEncoding + { + ContentType = "image/jpeg", + Style = ParameterStyle.Form, + Explode = false, + AllowReserved = false + }, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiEncoding), + SourceValue = null, + TargetValue = new OpenApiEncoding + { + ContentType = "image/jpeg", + Style = ParameterStyle.Form, + Explode = false, + AllowReserved = false + } + } + } + }; + + // Null target + yield return new object[] + { + "Null target", + new OpenApiEncoding + { + ContentType = "image/jpeg", + Style = ParameterStyle.Form, + Explode = false, + AllowReserved = false + }, + null, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiEncoding), + TargetValue = null, + SourceValue = new OpenApiEncoding + { + ContentType = "image/jpeg", + Style = ParameterStyle.Form, + Explode = false, + AllowReserved = false + } + } + } + }; + } + + + [Theory(Skip = "Need to fix")] + [MemberData(nameof(GetTestCasesForOpenApiEncodingComparerShouldSucceed))] + public void OpenApiEncodingComparerShouldSucceed( + string testCaseName, + OpenApiEncoding source, + OpenApiEncoding target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiEncodingComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiExampleComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiExampleComparerTests.cs new file mode 100644 index 000000000..716c75783 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiExampleComparerTests.cs @@ -0,0 +1,461 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiExampleComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiExampleComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiExampleComparerShouldSucceed() + { + yield return new object[] + { + "Differences in description, summary and external value", + new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + ExternalValue = "http://localhost/1" + }, + new OpenApiExample + { + Description = "Test description updated", + Summary = "Test summary updated", + ExternalValue = "http://localhost/2" + }, + new List + { + new OpenApiDifference + { + Pointer = "#/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Test description", + TargetValue = "Test description updated" + }, + new OpenApiDifference + { + Pointer = "#/summary", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Test summary", + TargetValue = "Test summary updated" + }, + new OpenApiDifference + { + Pointer = "#/externalValue", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "http://localhost/1", + TargetValue = "http://localhost/2" + } + } + }; + + yield return new object[] + { + "Null source", + null, + new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + ExternalValue = "http://localhost/1" + }, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiExample), + SourceValue = null, + TargetValue = new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + ExternalValue = "http://localhost/1" + } + } + } + }; + + yield return new object[] + { + "Null target", + new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + ExternalValue = "http://localhost/1" + }, + null, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiExample), + TargetValue = null, + SourceValue = new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + ExternalValue = "http://localhost/1" + } + } + } + }; + + yield return new object[] + { + "Difference in value", + new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + Value = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + }, + new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + Value = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relUpdated"] = new OpenApiString("sampleRel1") + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/value", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IOpenApiAny), + TargetValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["relUpdated"] = new OpenApiString("sampleRel1") + } + } + }, + SourceValue = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + } + } + }; + + yield return new object[] + { + "No differences", + new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + Value = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + }, + new OpenApiExample + { + Description = "Test description", + Summary = "Test summary", + Value = new OpenApiObject + { + ["status"] = new OpenApiString("Status1"), + ["id"] = new OpenApiString("v1"), + ["links"] = new OpenApiArray + { + new OpenApiObject + { + ["href"] = new OpenApiString("http://example.com/1"), + ["rel"] = new OpenApiString("sampleRel1") + } + } + } + }, + new List() + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiExampleComparerShouldSucceed))] + public void OpenApiExampleComparerShouldSucceed( + string testCaseName, + OpenApiExample source, + OpenApiExample target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiExampleComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiInfoComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiInfoComparerTests.cs new file mode 100644 index 000000000..b5c9b70df --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiInfoComparerTests.cs @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiInfoComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiInfoComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiInfoComparerShouldSucceed() + { + yield return new object[] + { + "Differences in title, description, version and tos", + new OpenApiInfo + { + Title = "Test title", + Description = "Test description", + Version = "Test version", + TermsOfService = new Uri("http://localhost/1") + }, + new OpenApiInfo + { + Title = "Test title updated", + Description = "Test description updated", + Version = "Test version updated", + TermsOfService = new Uri("http://localhost/2") + }, + new List + { + new OpenApiDifference + { + Pointer = "#/title", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + TargetValue = "Test title updated", + SourceValue = "Test title" + }, + new OpenApiDifference + { + Pointer = "#/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + TargetValue = "Test description updated", + SourceValue = "Test description" + }, + new OpenApiDifference + { + Pointer = "#/version", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + TargetValue = "Test version updated", + SourceValue = "Test version" + }, + new OpenApiDifference + { + Pointer = "#/termsOfService", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + TargetValue = new Uri("http://localhost/2"), + SourceValue = new Uri("http://localhost/1") + } + } + }; + } + + [Theory(Skip = "Need to fix")] + [MemberData(nameof(GetTestCasesForOpenApiInfoComparerShouldSucceed))] + public void OpenApiInfoComparerShouldSucceed( + string testCaseName, + OpenApiInfo source, + OpenApiInfo target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiInfoComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiParameterComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiParameterComparerTests.cs new file mode 100644 index 000000000..a2455bbae --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiParameterComparerTests.cs @@ -0,0 +1,477 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiParameterComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiParameterComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiParameterComparerShouldSucceed() + { + // Source and Target are null + yield return new object[] + { + "Source and Target are null", + null, + null, + new List() + }; + + // Source is null + yield return new object[] + { + "Source is null", + null, + new OpenApiParameter + { + Name = "pathParam", + In = ParameterLocation.Path + }, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiParameter), + SourceValue = null, + TargetValue = new OpenApiParameter + { + Name = "pathParam", + In = ParameterLocation.Path + } + } + } + }; + + // Target is null + yield return new object[] + { + "Target is null", + new OpenApiParameter + { + Name = "pathParam", + In = ParameterLocation.Path + }, + null, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiParameter), + TargetValue = null, + SourceValue = new OpenApiParameter + { + Name = "pathParam", + In = ParameterLocation.Path + } + } + } + }; + + // Differences in target and source + yield return new object[] + { + "Differences in target and source", + new OpenApiParameter + { + Name = "pathParam", + Description = "Sample path parameter description", + In = ParameterLocation.Path, + Required = true, + AllowEmptyValue = true, + AllowReserved = true, + Style = ParameterStyle.Form, + Deprecated = false, + Explode = false, + Schema = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + new OpenApiParameter + { + Name = "pathParamUpdate", + Description = "Updated Sample path parameter description", + In = ParameterLocation.Query, + Required = false, + AllowEmptyValue = false, + AllowReserved = false, + Style = ParameterStyle.Label, + Deprecated = true, + Explode = true, + Schema = new OpenApiSchema + { + Type = "bool", + MaxLength = 15 + }, + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/content/text~1plain", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiMediaType), + TargetValue = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + }, + SourceValue = null + }, + new OpenApiDifference + { + Pointer = "#/content/application~1json", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiMediaType), + SourceValue = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Sample path parameter description", + TargetValue = "Updated Sample path parameter description" + }, + new OpenApiDifference + { + Pointer = "#/required", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = false, + SourceValue = true + }, + new OpenApiDifference + { + Pointer = "#/name", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "pathParam", + TargetValue = "pathParamUpdate" + }, + new OpenApiDifference + { + Pointer = "#/deprecated", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = true, + SourceValue = false + }, + new OpenApiDifference + { + Pointer = "#/allowEmptyValue", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = false, + SourceValue = true + }, + new OpenApiDifference + { + Pointer = "#/explode", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = true, + SourceValue = false + }, + new OpenApiDifference + { + Pointer = "#/allowReserved", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = false, + SourceValue = true + }, + new OpenApiDifference + { + Pointer = "#/style", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(ParameterStyle), + SourceValue = ParameterStyle.Form, + TargetValue = ParameterStyle.Label + }, + new OpenApiDifference + { + Pointer = "#/in", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(ParameterLocation), + SourceValue = ParameterLocation.Path, + TargetValue = ParameterLocation.Query + }, + + new OpenApiDifference + { + Pointer = "#/schema/type", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "string", + TargetValue = "bool" + } + } + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiParameterComparerShouldSucceed))] + public void OpenApiParameterComparerShouldSucceed( + string testCaseName, + OpenApiParameter source, + OpenApiParameter target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiParameterComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiParametersComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiParametersComparerTests.cs new file mode 100644 index 000000000..6d7c2129a --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiParametersComparerTests.cs @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiParametersComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiParametersComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiParametersComparerShouldSucceed() + { + // Source and Target are null + yield return new object[] + { + "Source and Target are null", + null, + null, + new List() + }; + + // Source and Target are empty + yield return new object[] + { + "Source and Target are null", + new List(), + new List(), + new List() + }; + + // Source is null + yield return new object[] + { + "Source is null", + null, + new List + { + new OpenApiParameter + { + Name = "pathParam1", + In = ParameterLocation.Path + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IList), + SourceValue = null, + TargetValue = new List + { + new OpenApiParameter + { + Name = "pathParam1", + In = ParameterLocation.Path + } + } + } + } + }; + + // Target is null + yield return new object[] + { + "Target is null", + new List + { + new OpenApiParameter + { + Name = "pathParam1", + In = ParameterLocation.Path + } + }, + null, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IList), + TargetValue = null, + SourceValue = new List + { + new OpenApiParameter + { + Name = "pathParam1", + In = ParameterLocation.Path + } + } + } + } + }; + + // New, Removed and Updated Parameters + yield return new object[] + { + "New, Removed and Updated Parameters", + new List + { + new OpenApiParameter + { + Name = "pathParam1", + In = ParameterLocation.Path + }, + new OpenApiParameter + { + Name = "pathParam2", + In = ParameterLocation.Path + }, + new OpenApiParameter + { + Name = "pathParam3", + In = ParameterLocation.Path, + Description = "Sample path parameter description" + }, + new OpenApiParameter + { + Name = "queryParam1", + In = ParameterLocation.Query + }, + new OpenApiParameter + { + Name = "queryParam2", + In = ParameterLocation.Query + } + }, + new List + { + new OpenApiParameter + { + Name = "queryParam1", + In = ParameterLocation.Query + }, + new OpenApiParameter + { + Name = "pathParam1", + In = ParameterLocation.Path + }, + new OpenApiParameter + { + Name = "queryParam3", + In = ParameterLocation.Query + }, + new OpenApiParameter + { + Name = "pathParam3", + In = ParameterLocation.Path, + Description = "Updated Sample path parameter description" + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/4", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiParameter), + TargetValue = null, + SourceValue = new OpenApiParameter + { + Name = "queryParam2", + In = ParameterLocation.Query + } + }, + new OpenApiDifference + { + Pointer = "#/1", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiParameter), + TargetValue = null, + SourceValue = new OpenApiParameter + { + Name = "pathParam2", + In = ParameterLocation.Path + } + }, + new OpenApiDifference + { + Pointer = "#/2", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiParameter), + SourceValue = null, + TargetValue = new OpenApiParameter + { + Name = "queryParam3", + In = ParameterLocation.Query + } + }, + new OpenApiDifference + { + Pointer = "#/3/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Sample path parameter description", + TargetValue = "Updated Sample path parameter description" + } + } + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiParametersComparerShouldSucceed))] + public void OpenApiParametersComparerShouldSucceed( + string testCaseName, + IList source, + IList target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiParametersComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiRequestBodyComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiRequestBodyComparerTests.cs new file mode 100644 index 000000000..957708e8c --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiRequestBodyComparerTests.cs @@ -0,0 +1,587 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiRequestBodyComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiRequestBodyComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiRequestBodyComparerShouldSucceed() + { + // Differences in description and Required + yield return new object[] + { + "Differences in description and Required", + new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + new OpenApiRequestBody + { + Description = "udpated description", + Required = false, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + TargetValue = "udpated description", + SourceValue = "description" + }, + new OpenApiDifference + { + Pointer = "#/required", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(bool?), + TargetValue = false, + SourceValue = true + } + } + }; + + // Differences in Content + yield return new object[] + { + "Differences in Content", + new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/content/application~1xml", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiMediaType), + SourceValue = null, + TargetValue = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + }, + new OpenApiDifference + { + Pointer = "#/content/application~1json", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiMediaType), + TargetValue = null, + SourceValue = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + }; + + // Null source + yield return new object[] + { + "Null source", + null, + new OpenApiRequestBody + { + Description = "udpated description", + Required = false, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiRequestBody), + SourceValue = null, + TargetValue = new OpenApiRequestBody + { + Description = "udpated description", + Required = false, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + } + } + }; + + // Null target + yield return new object[] + { + "Null target", + new OpenApiRequestBody + { + Description = "udpated description", + Required = false, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + null, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiRequestBody), + SourceValue = new OpenApiRequestBody + { + Description = "udpated description", + Required = false, + Content = + { + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + }, + TargetValue = null + } + } + }; + + // Differences in reference id + yield return new object[] + { + "Differences in reference id", + new OpenApiRequestBody + { + Reference = new OpenApiReference + { + Id = "Id", + Type = ReferenceType.RequestBody + }, + + Description = "description", + Required = true + }, + new OpenApiRequestBody + { + Reference = new OpenApiReference + { + Id = "NewId", + Type = ReferenceType.RequestBody + }, + + Description = "description", + Required = true + }, + new List + { + new OpenApiDifference + { + Pointer = "#/$ref", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiReference), + TargetValue = new OpenApiReference + { + Id = "NewId", + Type = ReferenceType.RequestBody + }, + SourceValue = new OpenApiReference + { + Id = "Id", + Type = ReferenceType.RequestBody + } + } + } + }; + + // Differences in schema + yield return new object[] + { + "Differences in schema", + new OpenApiRequestBody + { + Reference = new OpenApiReference + { + Id = "requestBody1", + Type = ReferenceType.RequestBody + }, + + Description = "description", + Required = true + }, + new OpenApiRequestBody + { + Reference = new OpenApiReference + { + Id = "requestBody1", + Type = ReferenceType.RequestBody + }, + + Description = "description", + Required = true + }, + new List + { + new OpenApiDifference + { + Pointer = "#/content/application~1json/schema/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/content/application~1json/schema/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/content/application~1json/schema/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/content/application~1json/schema/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + } + } + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiRequestBodyComparerShouldSucceed))] + public void OpenApiRequestBodyComparerShouldSucceed( + string testCaseName, + OpenApiRequestBody source, + OpenApiRequestBody target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiRequestBodyComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiResponsesComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiResponsesComparerTests.cs new file mode 100644 index 000000000..7cbf858b3 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiResponsesComparerTests.cs @@ -0,0 +1,818 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiResponsesComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + Responses = new Dictionary + { + ["responseObject1"] = new OpenApiResponse + { + Description = "description", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["responseObject2"] = new OpenApiResponse + { + Description = "description", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + Responses = new Dictionary + { + ["responseObject1"] = new OpenApiResponse + { + Description = "description", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["responseObject2"] = new OpenApiResponse + { + Description = "description", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiResponsesComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiResponsesComparerShouldSucceed() + { + // Differences in description + yield return new object[] + { + "Differences in description", + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + } + }, + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "string" + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/200/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "A complex object array response", + TargetValue = "An updated complex object array response" + } + } + }; + + // New response code + yield return new object[] + { + "New response code", + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + new OpenApiResponses + { + { + "400", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/400", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiResponse), + SourceValue = null, + TargetValue = new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/200", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiResponse), + TargetValue = null, + SourceValue = new OpenApiResponse + { + Description = "A complex object array response", + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + } + }; + + // Differences in Content + yield return new object[] + { + "Differences in Content", + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Content = + { + ["text/plain"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/200/content/application~1json", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiMediaType), + SourceValue = null, + TargetValue = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/200/content/text~1plain", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiMediaType), + TargetValue = null, + SourceValue = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + }; + + // Null source + yield return new object[] + { + "Null source", + null, + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IDictionary), + SourceValue = null, + TargetValue = new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + } + } + } + }; + + // Null target + yield return new object[] + { + "Null target", + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + }, + null, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(IDictionary), + TargetValue = null, + SourceValue = new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "An updated complex object array response", + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + } + } + } + } + } + } + }; + + // Differences in reference id + yield return new object[] + { + "Differences in reference id", + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Reference = new OpenApiReference + { + Id = "responseObject1", + Type = ReferenceType.Response + } + } + } + }, + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Reference = new OpenApiReference + { + Id = "responseObject2", + Type = ReferenceType.Response + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/200/$ref", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiReference), + SourceValue = new OpenApiReference + { + Id = "responseObject1", + Type = ReferenceType.Response + }, + TargetValue = new OpenApiReference + { + Id = "responseObject2", + Type = ReferenceType.Response + } + } + } + }; + + // Differences in schema + yield return new object[] + { + "Differences in schema", + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Reference = new OpenApiReference + { + Id = "responseObject1", + Type = ReferenceType.Response + } + } + } + }, + new OpenApiResponses + { + { + "200", + new OpenApiResponse + { + Description = "A complex object array response", + Reference = new OpenApiReference + { + Id = "responseObject1", + Type = ReferenceType.Response + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/200/content/application~1json/schema/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = "#/200/content/application~1json/schema/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = + "#/200/content/application~1json/schema/properties/property6/properties/property6/properties/property5", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = null, + TargetValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + } + }, + new OpenApiDifference + { + Pointer = + "#/200/content/application~1json/schema/properties/property6/properties/property6/properties/property7", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiSchema), + SourceValue = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + TargetValue = null + } + } + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiResponsesComparerShouldSucceed))] + public void OpenApiResponsesComparerShouldSucceed( + string testCaseName, + OpenApiResponses source, + OpenApiResponses target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiDictionaryComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiSecurityRequirementComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiSecurityRequirementComparerTests.cs new file mode 100644 index 000000000..840b74bea --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiSecurityRequirementComparerTests.cs @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiSecurityRequirementComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + SecuritySchemes = new Dictionary + { + { + "scheme1", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + { + "scheme2", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + { + "scheme3", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + SecuritySchemes = new Dictionary + { + { + "scheme1", new OpenApiSecurityScheme + { + Description = "Test Updated", + Name = "Test" + } + }, + { + "scheme2", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + { + "scheme4", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + } + } + } + }; + + public OpenApiSecurityRequirementComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiSecurityRequirementComparerShouldSucceed() + { + yield return new object[] + { + "New Removed And updated schemes", + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme1"} + } + ] = new List + { + "scope1", + "scope2", + "scope3" + }, + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme2"} + } + ] = new List + { + "scope4", + "scope5" + }, + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme3"} + } + ] = new List() + }, + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme1"} + } + ] = new List + { + "scope1", + "scope2", + "scope3" + }, + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme2"} + } + ] = new List + { + "scope4", + "scope5" + }, + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme4"} + } + ] = new List() + }, + new List + { + new OpenApiDifference + { + Pointer = "#/scheme4", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(IList), + SourceValue = null, + TargetValue = new List() + }, + new OpenApiDifference + { + Pointer = "#/scheme1/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Test", + TargetValue = "Test Updated" + }, + new OpenApiDifference + { + Pointer = "#/scheme3", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(IList), + SourceValue = new List(), + TargetValue = null + } + } + }; + + yield return new object[] + { + "Source and target are null", + null, + null, + new List() + }; + + yield return new object[] + { + "Source is null", + null, + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme1"} + } + ] = new List() + }, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiSecurityRequirement), + SourceValue = null, + TargetValue = new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme1" + } + } + ] = new List() + } + } + } + }; + + yield return new object[] + { + "Target is null", + new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "scheme1"} + } + ] = new List() + }, + null, + new List + { + new OpenApiDifference + { + Pointer = "#/", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiSecurityRequirement), + SourceValue = new OpenApiSecurityRequirement + { + [ + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "scheme1" + } + } + ] = new List() + }, + TargetValue = null + } + } + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiSecurityRequirementComparerShouldSucceed))] + public void OpenApiSecurityRequirementComparerShouldSucceed( + string testCaseName, + OpenApiSecurityRequirement source, + OpenApiSecurityRequirement target, + List expectedDifferences) + + + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiSecurityRequirementComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiSecuritySchemeComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiSecuritySchemeComparerTests.cs new file mode 100644 index 000000000..1e0392843 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiSecuritySchemeComparerTests.cs @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiSecuritySchemeComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + SecuritySchemes = new Dictionary + { + { + "scheme1", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test", + Flows = new OpenApiOAuthFlows + { + Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/1") + }, + AuthorizationCode = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + } + } + }, + { + "scheme2", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + { + "scheme3", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + SecuritySchemes = new Dictionary + { + { + "scheme1", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test", + Flows = new OpenApiOAuthFlows + { + Implicit = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/3") + }, + ClientCredentials = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + } + } + }, + { + "scheme2", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + }, + { + "scheme4", new OpenApiSecurityScheme + { + Description = "Test", + Name = "Test" + } + } + } + } + }; + + public OpenApiSecuritySchemeComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiSecuritySchemeComparerShouldSucceed() + { + yield return new object[] + { + "Updated Type, Description, Name, In, BearerFormat, OpenIdConnectUrl", + new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + Description = "Test Description", + Name = "Test Name", + In = ParameterLocation.Path, + OpenIdConnectUrl = new Uri("http://localhost:1"), + BearerFormat = "Test Format" + }, + new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Description = "Test Description Updated", + Name = "Test Name Updated", + Scheme = "basic" + }, + new List + { + new OpenApiDifference + { + Pointer = "#/type", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(SecuritySchemeType), + SourceValue = SecuritySchemeType.ApiKey, + TargetValue = SecuritySchemeType.Http + }, + new OpenApiDifference + { + Pointer = "#/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Test Description", + TargetValue = "Test Description Updated" + }, + new OpenApiDifference + { + Pointer = "#/name", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Test Name", + TargetValue = "Test Name Updated" + }, + new OpenApiDifference + { + Pointer = "#/in", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(ParameterLocation), + SourceValue = ParameterLocation.Path, + TargetValue = ParameterLocation.Query + }, + new OpenApiDifference + { + Pointer = "#/bearerFormat", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "Test Format", + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/openIdConnectUrl", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + SourceValue = new Uri("http://localhost:1"), + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/scheme", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = null, + TargetValue = "basic" + } + } + }; + + yield return new object[] + { + "Difference in reference id", + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "scheme1", + Type = ReferenceType.SecurityScheme + } + }, + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "scheme2", + Type = ReferenceType.SecurityScheme + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/$ref", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiReference), + SourceValue = new OpenApiReference + { + Id = "scheme1", + Type = ReferenceType.SecurityScheme + }, + TargetValue = new OpenApiReference + { + Id = "scheme2", + Type = ReferenceType.SecurityScheme + } + } + } + }; + + yield return new object[] + { + "New, Removed and Updated OAuthFlows", + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "scheme1", + Type = ReferenceType.SecurityScheme + } + }, + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "scheme1", + Type = ReferenceType.SecurityScheme + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/flows/implicit/authorizationUrl", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + SourceValue = new Uri("http://localhost/1"), + TargetValue = new Uri("http://localhost/3") + }, + new OpenApiDifference + { + Pointer = "#/flows/authorizationCode", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + }, + TargetValue = null + }, + new OpenApiDifference + { + Pointer = "#/flows/clientCredentials", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(OpenApiOAuthFlow), + SourceValue = null, + TargetValue = new OpenApiOAuthFlow + { + AuthorizationUrl = new Uri("http://localhost/2") + } + } + } + }; + } + + [Theory(Skip = "Need to fix")] + [MemberData(nameof(GetTestCasesForOpenApiSecuritySchemeComparerShouldSucceed))] + public void OpenApiSecuritySchemeComparerShouldSucceed( + string testCaseName, + OpenApiSecurityScheme source, + OpenApiSecurityScheme target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiSecuritySchemeComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiServerVariableComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiServerVariableComparerTests.cs new file mode 100644 index 000000000..4b2e17dc2 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiServerVariableComparerTests.cs @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiServerVariableComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiServerVariableComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiServerVariableComparerShouldSucceed() + { + // Differences in default and description + yield return new object[] + { + "Differences in default and description", + new OpenApiServerVariable + { + Default = "8443", + Enum = new List + { + "8443", + "443" + }, + Description = "test description" + }, + new OpenApiServerVariable + { + Default = "1003", + Enum = new List + { + "8443", + "443" + }, + Description = "test description updated" + }, + new List + { + new OpenApiDifference + { + Pointer = "#/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "test description", + TargetValue = "test description updated" + }, + new OpenApiDifference + { + Pointer = "#/default", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "8443", + TargetValue = "1003" + } + } + }; + } + + [Theory] + [MemberData(nameof(GetTestCasesForOpenApiServerVariableComparerShouldSucceed))] + public void OpenApiServerVariableComparerShouldSucceed( + string testCaseName, + OpenApiServerVariable source, + OpenApiServerVariable target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiServerVariableComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiServersComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiServersComparerTests.cs new file mode 100644 index 000000000..66ae5c102 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiServersComparerTests.cs @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiServersComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiServersComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiServersComparerShouldSucceed() + { + // Differences in description + yield return new object[] + { + "Differences in description", + new List + { + new OpenApiServer + { + Description = "description1", + Url = "https://{username}.example.com:{port}/{basePath}", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + } + }, + new List + { + new OpenApiServer + { + Description = "description2", + Url = "https://{username}.example.com:{port}/{basePath}", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/0/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "description1", + TargetValue = "description2" + } + } + }; + + // New and Removed server + yield return new object[] + { + "New and Removed server", + new List + { + new OpenApiServer + { + Description = "description1", + Url = "https://{username}.example.com:{port}/{basePath}", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + } + }, + new List + { + new OpenApiServer + { + Description = "description1", + Url = "https://{username}.example.com:{port}/test", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + }, + new OpenApiServer + { + Description = "description3", + Url = "https://{username}.example.com:{port}/{basePath}/test", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/0", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiServer), + SourceValue = null, + TargetValue = new OpenApiServer + { + Description = "description1", + Url = "https://{username}.example.com:{port}/test", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/1", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Add, + OpenApiComparedElementType = typeof(OpenApiServer), + SourceValue = null, + TargetValue = new OpenApiServer + { + Description = "description3", + Url = "https://{username}.example.com:{port}/{basePath}/test", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + } + }, + new OpenApiDifference + { + Pointer = "#/0", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Remove, + OpenApiComparedElementType = typeof(OpenApiServer), + TargetValue = null, + SourceValue = new OpenApiServer + { + Description = "description1", + Url = "https://{username}.example.com:{port}/{basePath}", + Variables = new Dictionary + { + ["username"] = new OpenApiServerVariable + { + Default = "unknown", + Description = "variableDescription1" + }, + ["port"] = new OpenApiServerVariable + { + Default = "8443", + Description = "variableDescription2", + Enum = new List + { + "443", + "8443" + } + }, + ["basePath"] = new OpenApiServerVariable + { + Default = "v1" + } + } + } + } + } + }; + } + + [Theory(Skip = "Need to fix")] + [MemberData(nameof(GetTestCasesForOpenApiServersComparerShouldSucceed))] + public void OpenApiServersComparerShouldSucceed( + string testCaseName, + IList source, + IList target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiServersComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiTagComparerTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiTagComparerTests.cs new file mode 100644 index 000000000..ab8173577 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiTagComparerTests.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.OpenApi.Tests.Services +{ + [Collection("DefaultSettings")] + public class OpenApiTagComparerTests + { + private readonly ITestOutputHelper _output; + + private readonly OpenApiDocument _sourceDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property7"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + private readonly OpenApiDocument _targetDocument = new OpenApiDocument + { + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["schemaObject1"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject2" + } + } + } + }, + ["schemaObject2"] = new OpenApiSchema + { + Properties = new Dictionary + { + ["property2"] = new OpenApiSchema + { + Type = "integer" + }, + ["property5"] = new OpenApiSchema + { + Type = "string", + MaxLength = 15 + }, + ["property6"] = new OpenApiSchema + { + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "schemaObject1" + } + } + } + } + }, + RequestBodies = new Dictionary + { + ["requestBody1"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + }, + ["requestBody2"] = new OpenApiRequestBody + { + Description = "description", + Required = true, + Content = + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "schemaObject1", + Type = ReferenceType.Schema + } + } + } + } + } + } + } + }; + + public OpenApiTagComparerTests(ITestOutputHelper output) + { + _output = output; + } + + public static IEnumerable GetTestCasesForOpenApiTagComparerShouldSucceed() + { + // Differences in name, description and external docs + yield return new object[] + { + "Differences in name, description and external docs", + new OpenApiTag + { + Description = "test description", + Name = "test name", + ExternalDocs = new OpenApiExternalDocs + { + Description = "test description", + Url = new Uri("http://localhost/doc") + } + }, + new OpenApiTag + { + Description = "test description updated", + Name = "test name updated", + ExternalDocs = new OpenApiExternalDocs + { + Description = "test description updated", + Url = new Uri("http://localhost/updated") + } + }, + new List + { + new OpenApiDifference + { + Pointer = "#/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "test description", + TargetValue = "test description updated" + }, + new OpenApiDifference + { + Pointer = "#/name", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "test name", + TargetValue = "test name updated" + }, + new OpenApiDifference + { + Pointer = "#/externalDocs/description", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(string), + SourceValue = "test description", + TargetValue = "test description updated" + }, + new OpenApiDifference + { + Pointer = "#/externalDocs/url", + OpenApiDifferenceOperation = OpenApiDifferenceOperation.Update, + OpenApiComparedElementType = typeof(Uri), + SourceValue = new Uri("http://localhost/doc"), + TargetValue = new Uri("http://localhost/updated") + } + } + }; + } + + [Theory(Skip = "Need to fix")] + [MemberData(nameof(GetTestCasesForOpenApiTagComparerShouldSucceed))] + public void OpenApiTagServerVariableComparerShouldSucceed( + string testCaseName, + OpenApiTag source, + OpenApiTag target, + List expectedDifferences) + { + _output.WriteLine(testCaseName); + + var comparisonContext = new ComparisonContext(new OpenApiComparerFactory(), _sourceDocument, + _targetDocument); + var comparer = new OpenApiTagComparer(); + comparer.Compare(source, target, comparisonContext); + + var differences = comparisonContext.OpenApiDifferences.ToList(); + differences.Count().Should().Be(expectedDifferences.Count); + + differences.Should().BeEquivalentTo(expectedDifferences); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs index 8cec27ad4..45cc9c3d9 100644 --- a/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs +++ b/test/Microsoft.OpenApi.Tests/Services/OpenApiValidatorTests.cs @@ -35,6 +35,7 @@ public void ResponseMustHaveADescription() Title = "foo", Version = "1.2.2" }; + openApiDocument.Paths = new OpenApiPaths(); openApiDocument.Paths.Add( "/test", new OpenApiPathItem @@ -55,7 +56,7 @@ public void ResponseMustHaveADescription() var walker = new OpenApiWalker(validator); walker.Walk(openApiDocument); - validator.Errors.ShouldBeEquivalentTo( + validator.Errors.Should().BeEquivalentTo( new List { new OpenApiValidatorError(nameof(OpenApiResponseRules.ResponseRequiredFields),"#/paths/~1test/get/responses/200/description", @@ -66,13 +67,14 @@ public void ResponseMustHaveADescription() [Fact] public void ServersShouldBeReferencedByIndex() { - var openApiDocument = new OpenApiDocument(); - openApiDocument.Info = new OpenApiInfo() + var openApiDocument = new OpenApiDocument { - Title = "foo", - Version = "1.2.2" - }; - openApiDocument.Servers = new List { + Info = new OpenApiInfo() + { + Title = "foo", + Version = "1.2.2" + }, + Servers = new List { new OpenApiServer { Url = "http://example.org" @@ -80,14 +82,16 @@ public void ServersShouldBeReferencedByIndex() new OpenApiServer { - } + }, + }, + Paths = new OpenApiPaths() }; - + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); var walker = new OpenApiWalker(validator); walker.Walk(openApiDocument); - validator.Errors.ShouldBeEquivalentTo( + validator.Errors.Should().BeEquivalentTo( new List { new OpenApiValidatorError(nameof(OpenApiServerRules.ServerRequiredFields), "#/servers/1/url", @@ -100,7 +104,7 @@ public void ServersShouldBeReferencedByIndex() public void ValidateCustomExtension() { var ruleset = ValidationRuleSet.GetDefaultRuleSet(); - + ruleset.Add( new ValidationRule( (context, item) => @@ -111,11 +115,14 @@ public void ValidateCustomExtension() } })); - var openApiDocument = new OpenApiDocument(); - openApiDocument.Info = new OpenApiInfo() + var openApiDocument = new OpenApiDocument { - Title = "foo", - Version = "1.2.2" + Info = new OpenApiInfo() + { + Title = "foo", + Version = "1.2.2" + }, + Paths = new OpenApiPaths() }; var fooExtension = new FooExtension() @@ -124,13 +131,13 @@ public void ValidateCustomExtension() Baz = "baz" }; - openApiDocument.Info.Extensions.Add("x-foo",fooExtension); + openApiDocument.Info.Extensions.Add("x-foo", fooExtension); var validator = new OpenApiValidator(ruleset); var walker = new OpenApiWalker(validator); walker.Walk(openApiDocument); - validator.Errors.ShouldBeEquivalentTo( + validator.Errors.Should().BeEquivalentTo( new List { new OpenApiValidatorError("FooExtensionRule", "#/info/x-foo", "Don't say hey") @@ -145,7 +152,7 @@ internal class FooExtension : IOpenApiExtension, IOpenApiElement public string Bar { get; set; } - public void Write(IOpenApiWriter writer) + public void Write(IOpenApiWriter writer, OpenApiSpecVersion specVersion) { writer.WriteStartObject(); writer.WriteProperty("baz", Baz); @@ -153,4 +160,4 @@ public void Write(IOpenApiWriter writer) writer.WriteEndObject(); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/StringExtensions.cs b/test/Microsoft.OpenApi.Tests/StringExtensions.cs index c2741035e..5b01c97ac 100644 --- a/test/Microsoft.OpenApi.Tests/StringExtensions.cs +++ b/test/Microsoft.OpenApi.Tests/StringExtensions.cs @@ -21,4 +21,4 @@ public static string MakeLineBreaksEnvironmentNeutral(this string input) .Replace("\n", Environment.NewLine); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs index 0edfc93a7..d10eaf590 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiComponentsValidationTests.cs @@ -20,7 +20,7 @@ public void ValidateKeyMustMatchRegularExpressionInComponents() { // Arrange const string key = "%@abc"; - + OpenApiComponents components = new OpenApiComponents() { Responses = new Dictionary @@ -33,7 +33,7 @@ public void ValidateKeyMustMatchRegularExpressionInComponents() // Act bool result = !errors.Any(); - + // Assert Assert.False(result); diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs index ac499708d..ec6bba7b5 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiContactValidationTests.cs @@ -19,7 +19,7 @@ public void ValidateEmailFieldIsEmailAddressInContact() { // Arrange const string testEmail = "support/example.com"; - + OpenApiContact contact = new OpenApiContact() { Email = testEmail diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.cs index 4c1325894..fee728f76 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiExternalDocsValidationTests.cs @@ -24,7 +24,7 @@ public void ValidateUrlIsRequiredInExternalDocs() var errors = externalDocs.Validate(ValidationRuleSet.GetDefaultRuleSet()); // Assert - + bool result = !errors.Any(); Assert.False(result); diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs new file mode 100644 index 000000000..62fc2d1ce --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiHeaderValidationTests.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations.Rules; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiHeaderValidationTests + { + [Fact] + public void ValidateExampleShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + var header = new OpenApiHeader() + { + Required = true, + Example = new OpenApiInteger(55), + Schema = new OpenApiSchema() + { + Type = "string", + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(header); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + "#/example", + }); + } + + [Fact] + public void ValidateExamplesShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + + var header = new OpenApiHeader() + { + Required = true, + Schema = new OpenApiSchema() + { + Type = "object", + AdditionalProperties = new OpenApiSchema() + { + Type = "integer", + } + }, + Examples = + { + ["example0"] = new OpenApiExample() + { + Value = new OpenApiString("1"), + }, + ["example1"] = new OpenApiExample() + { + Value = new OpenApiObject() + { + ["x"] = new OpenApiInteger(2), + ["y"] = new OpenApiString("20"), + ["z"] = new OpenApiString("200") + } + }, + ["example2"] = new OpenApiExample() + { + Value = + new OpenApiArray() + { + new OpenApiInteger(3) + } + }, + ["example3"] = new OpenApiExample() + { + Value = new OpenApiObject() + { + ["x"] = new OpenApiInteger(4), + ["y"] = new OpenApiInteger(40), + } + }, + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(header); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + // #enum/0 is not an error since the spec allows + // representing an object using a string. + "#/examples/example1/value/y", + "#/examples/example1/value/z", + "#/examples/example2/value" + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs index 1e98a076a..1a58fff04 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiInfoValidationTests.cs @@ -18,9 +18,9 @@ public class OpenApiInfoValidationTests public void ValidateFieldIsRequiredInInfo() { // Arrange - string urlError = String.Format(SRResource.Validation_FieldIsRequired, "title", "info"); + string titleError = String.Format(SRResource.Validation_FieldIsRequired, "title", "info"); string versionError = String.Format(SRResource.Validation_FieldIsRequired, "version", "info"); - OpenApiInfo info = new OpenApiInfo(); + var info = new OpenApiInfo(); // Act var errors = info.Validate(ValidationRuleSet.GetDefaultRuleSet()); @@ -32,7 +32,7 @@ public void ValidateFieldIsRequiredInInfo() Assert.False(result); Assert.NotNull(errors); - Assert.Equal(new[] { urlError, versionError }, errors.Select(e => e.Message)); + Assert.Equal(new[] { titleError, versionError }, errors.Select(e => e.Message)); } } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiMediaTypeValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiMediaTypeValidationTests.cs new file mode 100644 index 000000000..4a5ca1a6b --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiMediaTypeValidationTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations.Rules; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiMediaTypeValidationTests + { + [Fact] + public void ValidateExampleShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + var mediaType = new OpenApiMediaType() + { + Example = new OpenApiInteger(55), + Schema = new OpenApiSchema() + { + Type = "string", + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(mediaType); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + "#/example", + }); + } + + [Fact] + public void ValidateExamplesShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + + var mediaType = new OpenApiMediaType() + { + Schema = new OpenApiSchema() + { + Type = "object", + AdditionalProperties = new OpenApiSchema() + { + Type = "integer", + } + }, + Examples = + { + ["example0"] = new OpenApiExample() + { + Value = new OpenApiString("1"), + }, + ["example1"] = new OpenApiExample() + { + Value = new OpenApiObject() + { + ["x"] = new OpenApiInteger(2), + ["y"] = new OpenApiString("20"), + ["z"] = new OpenApiString("200") + } + }, + ["example2"] = new OpenApiExample() + { + Value = + new OpenApiArray() + { + new OpenApiInteger(3) + } + }, + ["example3"] = new OpenApiExample() + { + Value = new OpenApiObject() + { + ["x"] = new OpenApiInteger(4), + ["y"] = new OpenApiInteger(40), + } + }, + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(mediaType); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + // #enum/0 is not an error since the spec allows + // representing an object using a string. + "#/examples/example1/value/y", + "#/examples/example1/value/z", + "#/examples/example2/value" + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs new file mode 100644 index 000000000..667286f0a --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiParameterValidationTests.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations.Rules; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + public class OpenApiParameterValidationTests + { + [Fact] + public void ValidateFieldIsRequiredInParameter() + { + // Arrange + string nameError = String.Format(SRResource.Validation_FieldIsRequired, "name", "parameter"); + string inError = String.Format(SRResource.Validation_FieldIsRequired, "in", "parameter"); + var parameter = new OpenApiParameter(); + + // Act + var errors = parameter.Validate(ValidationRuleSet.GetDefaultRuleSet()); + + // Assert + errors.Should().NotBeEmpty(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + nameError, + inError + }); + } + + [Fact] + public void ValidateRequiredIsTrueWhenInIsPathInParameter() + { + // Arrange + var parameter = new OpenApiParameter() + { + Name = "name", + In = ParameterLocation.Path + }; + + // Act + var errors = parameter.Validate(ValidationRuleSet.GetDefaultRuleSet()); + + // Assert + errors.Should().NotBeEmpty(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + "\"required\" must be true when parameter location is \"path\"" + }); + } + + [Fact] + public void ValidateExampleShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + var parameter = new OpenApiParameter() + { + Name = "parameter1", + In = ParameterLocation.Path, + Required = true, + Example = new OpenApiInteger(55), + Schema = new OpenApiSchema() + { + Type = "string", + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(parameter); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + "#/example", + }); + } + + [Fact] + public void ValidateExamplesShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + + var parameter = new OpenApiParameter() + { + Name = "parameter1", + In = ParameterLocation.Path, + Required = true, + Schema = new OpenApiSchema() + { + Type = "object", + AdditionalProperties = new OpenApiSchema() + { + Type = "integer", + } + }, + Examples = + { + ["example0"] = new OpenApiExample() + { + Value = new OpenApiString("1"), + }, + ["example1"] = new OpenApiExample() + { + Value = new OpenApiObject() + { + ["x"] = new OpenApiInteger(2), + ["y"] = new OpenApiString("20"), + ["z"] = new OpenApiString("200") + } + }, + ["example2"] = new OpenApiExample() + { + Value = + new OpenApiArray() + { + new OpenApiInteger(3) + } + }, + ["example3"] = new OpenApiExample() + { + Value = new OpenApiObject() + { + ["x"] = new OpenApiInteger(4), + ["y"] = new OpenApiInteger(40), + } + }, + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(parameter); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + // #enum/0 is not an error since the spec allows + // representing an object using a string. + "#/examples/example1/value/y", + "#/examples/example1/value/z", + "#/examples/example2/value" + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs index d6e30010b..3ed365c8d 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiReferenceValidationTests.cs @@ -44,7 +44,7 @@ public void ReferencedSchemaShouldOnlyBeValidatedOnce() { ["/"] = new OpenApiPathItem() { - Operations = new Dictionary + Operations = new Dictionary { [OperationType.Get] = new OpenApiOperation() { @@ -52,7 +52,7 @@ public void ReferencedSchemaShouldOnlyBeValidatedOnce() { ["200"] = new OpenApiResponse() { - Content = new Dictionary() + Content = new Dictionary() { ["application/json"] = new OpenApiMediaType() { @@ -67,11 +67,11 @@ public void ReferencedSchemaShouldOnlyBeValidatedOnce() }; // Act - var errors = document.Validate(new ValidationRuleSet() { new AlwaysFailRule()}); + var errors = document.Validate(new ValidationRuleSet() { new AlwaysFailRule() }); // Assert - Assert.True(errors.Count() == 1); + Assert.True(errors.Count() == 1); } [Fact] @@ -154,11 +154,11 @@ public void UnresolvedSchemaReferencedShouldNotBeValidated() } } - public class AlwaysFailRule : ValidationRule where T: IOpenApiElement + public class AlwaysFailRule : ValidationRule where T : IOpenApiElement { - public AlwaysFailRule() : base( (c,t) => c.CreateError("x","y")) + public AlwaysFailRule() : base((c, t) => c.CreateError("x", "y")) { - + } } } diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs new file mode 100644 index 000000000..91b1643fa --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiSchemaValidationTests.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations.Rules; +using Xunit; + +namespace Microsoft.OpenApi.Validations.Tests +{ + [Collection("DefaultSettings")] + public class OpenApiSchemaValidationTests + { + [Fact] + public void ValidateDefaultShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + var schema = new OpenApiSchema() + { + Default = new OpenApiInteger(55), + Type = "string", + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(schema); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + "#/default", + }); + } + + [Fact] + public void ValidateExampleAndDefaultShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + var schema = new OpenApiSchema() + { + Example = new OpenApiLong(55), + Default = new OpenApiPassword("1234"), + Type = "string", + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(schema); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + "#/default", + "#/example", + }); + } + + [Fact] + public void ValidateEnumShouldNotHaveDataTypeMismatchForSimpleSchema() + { + // Arrange + IEnumerable errors; + var schema = new OpenApiSchema() + { + Enum = + { + new OpenApiString("1"), + new OpenApiObject() + { + ["x"] = new OpenApiInteger(2), + ["y"] = new OpenApiString("20"), + ["z"] = new OpenApiString("200") + }, + new OpenApiArray() + { + new OpenApiInteger(3) + }, + new OpenApiObject() + { + ["x"] = new OpenApiInteger(4), + ["y"] = new OpenApiInteger(40), + }, + }, + Type = "object", + AdditionalProperties = new OpenApiSchema() + { + Type = "integer", + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(schema); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + // #enum/0 is not an error since the spec allows + // representing an object using a string. + "#/enum/1/y", + "#/enum/1/z", + "#/enum/2" + }); + } + + [Fact] + public void ValidateDefaultShouldNotHaveDataTypeMismatchForComplexSchema() + { + // Arrange + IEnumerable errors; + var schema = new OpenApiSchema() + { + Type = "object", + Properties = + { + ["property1"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "integer", + Format = "int64" + } + }, + ["property2"] = new OpenApiSchema() + { + Type = "array", + Items = new OpenApiSchema() + { + Type = "object", + AdditionalProperties = new OpenApiSchema() + { + Type = "boolean" + } + } + }, + ["property3"] = new OpenApiSchema() + { + Type = "string", + Format = "password" + }, + ["property4"] = new OpenApiSchema() + { + Type = "string" + } + }, + Default = new OpenApiObject() + { + ["property1"] = new OpenApiArray() + { + new OpenApiInteger(12), + new OpenApiLong(13), + new OpenApiString("1"), + }, + ["property2"] = new OpenApiArray() + { + new OpenApiInteger(2), + new OpenApiObject() + { + ["x"] = new OpenApiBoolean(true), + ["y"] = new OpenApiBoolean(false), + ["z"] = new OpenApiString("1234"), + } + }, + ["property3"] = new OpenApiPassword("123"), + ["property4"] = new OpenApiDateTime(DateTime.UtcNow) + } + }; + + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(schema); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Select(e => e.Message).Should().BeEquivalentTo(new[] + { + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + RuleHelpers.DataTypeMismatchedErrorMessage, + }); + errors.Select(e => e.Pointer).Should().BeEquivalentTo(new[] + { + "#/default/property1/0", + "#/default/property1/2", + "#/default/property2/0", + "#/default/property2/1/z", + "#/default/property4", + }); + } + + [Fact] + public void ValidateSchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator() + { + IEnumerable errors; + var components = new OpenApiComponents + { + Schemas = { + { + "schema1", + new OpenApiSchema + { + Type = "object", + Discriminator = new OpenApiDiscriminator { PropertyName = "property1" }, + Reference = new OpenApiReference { Id = "schema1" } + } + } + } + }; + // Act + var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); + var walker = new OpenApiWalker(validator); + walker.Walk(components); + + errors = validator.Errors; + bool result = !errors.Any(); + + // Assert + result.Should().BeFalse(); + errors.Should().BeEquivalentTo(new List + { + new OpenApiValidatorError(nameof(OpenApiSchemaRules.ValidateSchemaDiscriminator),"#/schemas/schema1/discriminator", + string.Format(SRResource.Validation_SchemaRequiredFieldListMustContainThePropertySpecifiedInTheDiscriminator, + "schema1", "property1")) + }); + } + } +} diff --git a/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs b/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs index a00f5e502..a039b39c2 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/OpenApiTagValidationTests.cs @@ -50,7 +50,7 @@ public void ValidateExtensionNameStartsWithXDashInTag() var validator = new OpenApiValidator(ValidationRuleSet.GetDefaultRuleSet()); validator.Visit(tag as IOpenApiExtensible); errors = validator.Errors; - bool result = !errors.Any(); + bool result = !errors.Any(); // Assert Assert.False(result); diff --git a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs index 13555b3f3..af259cf1b 100644 --- a/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs +++ b/test/Microsoft.OpenApi.Tests/Validations/ValidationRuleSetTests.cs @@ -43,7 +43,7 @@ public void DefaultRuleSetPropertyReturnsTheCorrectRules() Assert.NotEmpty(rules); // Update the number if you add new default rule(s). - Assert.Equal(14, rules.Count); + Assert.Equal(21, rules.Count); } } } diff --git a/test/Microsoft.OpenApi.Tests/Walkers/WalkerLocationTests.cs b/test/Microsoft.OpenApi.Tests/Walkers/WalkerLocationTests.cs index 933a25274..fc947da20 100644 --- a/test/Microsoft.OpenApi.Tests/Walkers/WalkerLocationTests.cs +++ b/test/Microsoft.OpenApi.Tests/Walkers/WalkerLocationTests.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. using System.Collections.Generic; +using System.Linq; using FluentAssertions; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; using Xunit; @@ -16,14 +18,13 @@ public class WalkerLocationTests public void LocateTopLevelObjects() { var doc = new OpenApiDocument(); - + var locator = new LocatorVisitor(); var walker = new OpenApiWalker(locator); walker.Walk(doc); - locator.Locations.ShouldBeEquivalentTo(new List { + locator.Locations.Should().BeEquivalentTo(new List { "#/servers", - "#/paths", "#/tags" }); } @@ -37,6 +38,7 @@ public void LocateTopLevelArrayItems() new OpenApiServer(), new OpenApiServer() }, + Paths = new OpenApiPaths(), Tags = new List() { new OpenApiTag() @@ -47,7 +49,7 @@ public void LocateTopLevelArrayItems() var walker = new OpenApiWalker(locator); walker.Walk(doc); - locator.Locations.ShouldBeEquivalentTo(new List { + locator.Locations.Should().BeEquivalentTo(new List { "#/servers", "#/servers/0", "#/servers/1", @@ -60,31 +62,33 @@ public void LocateTopLevelArrayItems() [Fact] public void LocatePathOperationContentSchema() { - var doc = new OpenApiDocument(); + var doc = new OpenApiDocument + { + Paths = new OpenApiPaths() + }; doc.Paths.Add("/test", new OpenApiPathItem() { Operations = new Dictionary() { - { OperationType.Get, new OpenApiOperation() + [OperationType.Get] = new OpenApiOperation() { Responses = new OpenApiResponses() { - { "200", new OpenApiResponse() { - Content = new Dictionary + ["200"] = new OpenApiResponse() + { + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType { - { "application/json", new OpenApiMediaType { - Schema = new OpenApiSchema - { - Type = "string" - } - } + Schema = new OpenApiSchema + { + Type = "string" } } } } } } - } } }); @@ -92,20 +96,22 @@ public void LocatePathOperationContentSchema() var walker = new OpenApiWalker(locator); walker.Walk(doc); - locator.Locations.ShouldBeEquivalentTo(new List { + locator.Locations.Should().BeEquivalentTo(new List { "#/servers", - "#/tags", "#/paths", "#/paths/~1test", "#/paths/~1test/get", - "#/paths/~1test/get/tags", "#/paths/~1test/get/responses", "#/paths/~1test/get/responses/200", "#/paths/~1test/get/responses/200/content", "#/paths/~1test/get/responses/200/content/application~1json", "#/paths/~1test/get/responses/200/content/application~1json/schema", + "#/paths/~1test/get/tags", + "#/tags", }); + + locator.Keys.Should().BeEquivalentTo(new List { "/test", "Get", "200", "application/json" }); } [Fact] @@ -114,7 +120,7 @@ public void WalkDOMWithCycles() var loopySchema = new OpenApiSchema() { Type = "object", - Properties = new Dictionary() + Properties = new Dictionary() { ["name"] = new OpenApiSchema() { Type = "string" } } @@ -124,6 +130,7 @@ public void WalkDOMWithCycles() var doc = new OpenApiDocument() { + Paths = new OpenApiPaths(), Components = new OpenApiComponents() { Schemas = new Dictionary @@ -137,7 +144,7 @@ public void WalkDOMWithCycles() var walker = new OpenApiWalker(locator); walker.Walk(doc); - locator.Locations.ShouldBeEquivalentTo(new List { + locator.Locations.Should().BeEquivalentTo(new List { "#/servers", "#/paths", "#/components", @@ -146,11 +153,109 @@ public void WalkDOMWithCycles() "#/tags" }); } + + /// + /// Walk document and discover all references to components, including those inside components + /// + [Fact] + public void LocateReferences() + { + + var baseSchema = new OpenApiSchema() + { + Reference = new OpenApiReference() + { + Id = "base", + Type = ReferenceType.Schema + }, + UnresolvedReference = false + }; + + var derivedSchema = new OpenApiSchema + { + AnyOf = new List() { baseSchema }, + Reference = new OpenApiReference() + { + Id = "derived", + Type = ReferenceType.Schema + }, + UnresolvedReference = false + }; + + var testHeader = new OpenApiHeader() + { + Schema = derivedSchema, + Reference = new OpenApiReference() + { + Id = "test-header", + Type = ReferenceType.Header + }, + UnresolvedReference = false + }; + + var doc = new OpenApiDocument + { + Paths = new OpenApiPaths() + { + ["/"] = new OpenApiPathItem() + { + Operations = new Dictionary() + { + [OperationType.Get] = new OpenApiOperation() + { + Responses = new OpenApiResponses() + { + ["200"] = new OpenApiResponse() + { + Content = new Dictionary() + { + ["application/json"] = new OpenApiMediaType() + { + Schema = derivedSchema + } + }, + Headers = new Dictionary() + { + ["test-header"] = testHeader + } + } + } + } + } + } + }, + Components = new OpenApiComponents() + { + Schemas = new Dictionary() + { + ["derived"] = derivedSchema, + ["base"] = baseSchema, + }, + Headers = new Dictionary() + { + ["test-header"] = testHeader + } + } + }; + + var locator = new LocatorVisitor(); + var walker = new OpenApiWalker(locator); + walker.Walk(doc); + + locator.Locations.Where(l => l.StartsWith("referenceAt:")).Should().BeEquivalentTo(new List { + "referenceAt: #/paths/~1/get/responses/200/content/application~1json/schema", + "referenceAt: #/paths/~1/get/responses/200/headers/test-header", + "referenceAt: #/components/schemas/derived/anyOf/0", + "referenceAt: #/components/headers/test-header/schema" + }); + } } internal class LocatorVisitor : OpenApiVisitorBase { public List Locations = new List(); + public List Keys = new List(); + public override void Visit(OpenApiInfo info) { Locations.Add(this.PathString); @@ -173,6 +278,7 @@ public override void Visit(OpenApiPaths paths) public override void Visit(OpenApiPathItem pathItem) { + Keys.Add(CurrentKeys.Path); Locations.Add(this.PathString); } @@ -183,20 +289,27 @@ public override void Visit(OpenApiResponses responses) public override void Visit(OpenApiOperation operation) { + Keys.Add(CurrentKeys.Operation.ToString()); Locations.Add(this.PathString); } public override void Visit(OpenApiResponse response) { + Keys.Add(CurrentKeys.Response); Locations.Add(this.PathString); } - public override void Visit(IDictionary content) + public override void Visit(IOpenApiReferenceable referenceable) + { + Locations.Add("referenceAt: " + this.PathString); + } + public override void Visit(IDictionary content) { Locations.Add(this.PathString); } public override void Visit(OpenApiMediaType mediaType) { + Keys.Add(CurrentKeys.Content); Locations.Add(this.PathString); } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index 7b3124193..47002ace2 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -126,7 +126,8 @@ public void OpenApiWorkspacesAllowDocumentsToReferenceEachOther_short() workspace.AddDocument("root", doc); workspace.AddDocument("common", CreateCommonDocument()); - doc.ResolveReferences(true); + var errors = doc.ResolveReferences(true); + Assert.Empty(errors); var schema = doc.Paths["/"].Operations[OperationType.Get].Responses["200"].Content["application/json"].Schema; Assert.False(schema.UnresolvedReference); @@ -180,6 +181,7 @@ public static OpenApiDocument CreatePathItem(this OpenApiDocument document, stri { var pathItem = new OpenApiPathItem(); config(pathItem); + document.Paths = new OpenApiPaths(); document.Paths.Add(path, pathItem); return document; } diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs index e4a90c280..06d95c9ad 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiJsonWriterTests.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Writers; @@ -50,7 +52,7 @@ public static IEnumerable WriteStringListAsJsonShouldMatchExpectedTest public void WriteStringListAsJsonShouldMatchExpected(string[] stringValues) { // Arrange - var outputString = new StringWriter(); + var outputString = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputString); // Act @@ -68,7 +70,7 @@ public void WriteStringListAsJsonShouldMatchExpected(string[] stringValues) JsonConvert.DeserializeObject(JsonConvert.SerializeObject(new List(stringValues))); // Assert - parsedObject.ShouldBeEquivalentTo(expectedObject); + parsedObject.Should().BeEquivalentTo(expectedObject); } public static IEnumerable WriteMapAsJsonShouldMatchExpectedTestCasesSimple() @@ -134,6 +136,17 @@ public static IEnumerable WriteMapAsJsonShouldMatchExpectedTestCasesCo } }; + // DateTime + yield return new object[] + { + new Dictionary + { + ["property1"] = new DateTime(1970, 01, 01), + ["property2"] = new DateTimeOffset(new DateTime(1970, 01, 01)), + ["property3"] = new DateTime(2018, 04, 03), + } + }; + // Nested map yield return new object[] { @@ -183,7 +196,12 @@ public static IEnumerable WriteMapAsJsonShouldMatchExpectedTestCasesCo private void WriteValueRecursive(OpenApiJsonWriter writer, object value) { - if (value == null || value.GetType().IsPrimitive || value is decimal || value is string) + if (value == null + || value.GetType().IsPrimitive + || value is decimal + || value is string + || value is DateTimeOffset + || value is DateTime) { writer.WriteValue(value); } @@ -218,7 +236,7 @@ private void WriteValueRecursive(OpenApiJsonWriter writer, object value) public void WriteMapAsJsonShouldMatchExpected(IDictionary inputMap) { // Arrange - var outputString = new StringWriter(); + var outputString = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputString); // Act @@ -228,7 +246,51 @@ public void WriteMapAsJsonShouldMatchExpected(IDictionary inputM var expectedObject = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(inputMap)); // Assert - parsedObject.ShouldBeEquivalentTo(expectedObject); + parsedObject.Should().BeEquivalentTo(expectedObject); + } + + public static IEnumerable WriteDateTimeAsJsonTestCases() + { + yield return new object[] + { + new DateTimeOffset(2018, 1, 1, 10, 20, 30, TimeSpan.Zero), + }; + + yield return new object[] + { + new DateTimeOffset(2018, 1, 1, 10, 20, 30, 100, TimeSpan.FromHours(14)), + }; + + yield return new object[] + { + DateTimeOffset.UtcNow + TimeSpan.FromDays(4) + }; + + yield return new object[] + { + DateTime.UtcNow + TimeSpan.FromDays(4) + }; + } + + [Theory] + [MemberData(nameof(WriteDateTimeAsJsonTestCases))] + public void WriteDateTimeAsJsonShouldMatchExpected(DateTimeOffset dateTimeOffset) + { + // Arrange + var outputString = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputString); + + // Act + writer.WriteValue(dateTimeOffset); + + var writtenString = outputString.GetStringBuilder().ToString(); + var expectedString = JsonConvert.SerializeObject(dateTimeOffset, new JsonSerializerSettings + { + DateFormatString = "yyyy-MM-ddTHH:mm:ss.fffffffK", + }); + + // Assert + writtenString.Should().Be(expectedString); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs index 9ad1aa322..2ecf93f42 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterAnyExtensionsTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Any; @@ -93,13 +94,14 @@ public void WriteOpenApiDoubleAsJsonWorks(double input) public void WriteOpenApiDateTimeAsJsonWorks(string inputString) { // Arrange - var input = DateTimeOffset.Parse(inputString); + var input = DateTimeOffset.Parse(inputString, CultureInfo.InvariantCulture); var dateTimeValue = new OpenApiDateTime(input); var json = WriteAsJson(dateTimeValue); + var expectedJson = "\"" + input.ToString("o") + "\""; // Assert - json.Should().Be(input.ToString("o")); + json.Should().Be(expectedJson); } [Theory] @@ -215,4 +217,4 @@ private static string WriteAsJson(IOpenApiAny any) return value.MakeLineBreaksEnvironmentNeutral(); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs index 4994de16c..60e598882 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System.Globalization; using System.IO; using FluentAssertions; using Microsoft.OpenApi.Writers; @@ -31,7 +32,7 @@ public OpenApiWriterSpecialCharacterTests(ITestOutputHelper output) public void WriteStringWithSpecialCharactersAsJsonWorks(string input, string expected) { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiJsonWriter(outputStringWriter); // Act @@ -60,10 +61,11 @@ public void WriteStringWithSpecialCharactersAsJsonWorks(string input, string exp [InlineData("true", " 'true'")] [InlineData("trailingspace ", " 'trailingspace '")] [InlineData(" trailingspace", " ' trailingspace'")] + [InlineData("terminal:", " 'terminal:'")] public void WriteStringWithSpecialCharactersAsYamlWorks(string input, string expected) { // Arrange - var outputStringWriter = new StringWriter(); + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputStringWriter); // Act @@ -74,4 +76,4 @@ public void WriteStringWithSpecialCharactersAsYamlWorks(string input, string exp actual.Should().Be(expected); } } -} \ No newline at end of file +} diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs index 757f9097b..a73fdbb9b 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiYamlWriterTests.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; using FluentAssertions; +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Writers; using Xunit; using Xunit.Abstractions; @@ -61,7 +64,7 @@ public static IEnumerable WriteStringListAsYamlShouldMatchExpectedTest public void WriteStringListAsYamlShouldMatchExpected(string[] stringValues, string expectedYaml) { // Arrange - var outputString = new StringWriter(); + var outputString = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputString); // Act @@ -171,6 +174,20 @@ public static IEnumerable WriteMapAsYamlShouldMatchExpectedTestCasesCo property11: ''" }; + // DateTime + yield return new object[] + { + new Dictionary + { + ["property1"] = new DateTime(1970, 01, 01), + ["property2"] = new DateTimeOffset(new DateTime(1970, 01, 01), TimeSpan.FromHours(3)), + ["property3"] = new DateTime(2018, 04, 03), + }, + @"property1: '1970-01-01T00:00:00.0000000' +property2: '1970-01-01T00:00:00.0000000+03:00' +property3: '2018-04-03T00:00:00.0000000'" + }; + // Nested map yield return new object[] { @@ -237,7 +254,12 @@ public static IEnumerable WriteMapAsYamlShouldMatchExpectedTestCasesCo private void WriteValueRecursive(OpenApiYamlWriter writer, object value) { - if (value == null || value.GetType().IsPrimitive || value is decimal || value is string) + if (value == null + || value.GetType().IsPrimitive + || value is decimal + || value is string + || value is DateTimeOffset + || value is DateTime) { writer.WriteValue(value); } @@ -272,7 +294,7 @@ private void WriteValueRecursive(OpenApiYamlWriter writer, object value) public void WriteMapAsYamlShouldMatchExpected(IDictionary inputMap, string expectedYaml) { // Arrange - var outputString = new StringWriter(); + var outputString = new StringWriter(CultureInfo.InvariantCulture); var writer = new OpenApiYamlWriter(outputString); // Act @@ -284,5 +306,340 @@ public void WriteMapAsYamlShouldMatchExpected(IDictionary inputM expectedYaml = expectedYaml.MakeLineBreaksEnvironmentNeutral(); actualYaml.Should().Be(expectedYaml); } + + public static IEnumerable WriteDateTimeAsJsonTestCases() + { + yield return new object[] + { + new DateTimeOffset(2018, 1, 1, 10, 20, 30, TimeSpan.Zero) + }; + + yield return new object[] + { + new DateTimeOffset(2018, 1, 1, 10, 20, 30, 100, TimeSpan.FromHours(14)) + }; + + yield return new object[] + { + DateTimeOffset.UtcNow + TimeSpan.FromDays(4) + }; + + yield return new object[] + { + DateTime.UtcNow + TimeSpan.FromDays(4) + }; + } + + [Theory] + [MemberData(nameof(WriteDateTimeAsJsonTestCases))] + public void WriteDateTimeAsJsonShouldMatchExpected(DateTimeOffset dateTimeOffset) + { + // Arrange + var outputString = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputString); + + // Act + writer.WriteValue(dateTimeOffset); + + var writtenString = outputString.GetStringBuilder().ToString(); + var expectedString = " '" + dateTimeOffset.ToString("o") + "'"; + + // Assert + writtenString.Should().Be(expectedString); + } + + [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() + { + 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} + } + }; + return doc; + } + + [Fact] + + public void WriteInlineRecursiveSchema() + { + // Arrange + var doc = CreateDocWithRecursiveSchemaReference(); + + 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' + 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 }); + + // Act + doc.SerializeAsV3(writer); + var actual = outputString.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + + private static OpenApiDocument CreateDocWithRecursiveSchemaReference() + { + var thingSchema = new OpenApiSchema() + { + Type = "object", + UnresolvedReference = false, + Reference = new OpenApiReference + { + Id = "thing", + Type = ReferenceType.Schema + } + }; + thingSchema.Properties["children"] = thingSchema; + + 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 = { + ["200"] = new OpenApiResponse { + Description = "OK", + Content = { + ["application/json"] = new OpenApiMediaType() { + Schema = thingSchema + } + } + } + } + } + } + } + }, + Components = new OpenApiComponents + { + Schemas = { + ["thing"] = thingSchema} + } + }; + return doc; + } + + [Fact] + public void WriteInlineRecursiveSchemav2() + { + // Arrange + var doc = CreateDocWithRecursiveSchemaReference(); + + var expected = +@"swagger: '2.0' +info: + title: Demo + version: 1.0.0 +paths: + /: + get: + produces: + - application/json + responses: + '200': + description: OK + 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.SerializeAsV2(writer); + var actual = outputString.GetStringBuilder().ToString(); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } + } -} \ No newline at end of file +} From ccbef092e33ae18f12286e44567fcf85c71af9f4 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Sun, 8 Nov 2020 14:50:38 +0000 Subject: [PATCH 14/32] WIP; add OpenApiReferencableExtensions --- .../OpenApiReferencableExtensions.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs new file mode 100644 index 000000000..941d9a5c9 --- /dev/null +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Services; +using Microsoft.OpenApi.Validations; + +namespace Microsoft.OpenApi.Extensions +{ + /// + /// TODO: tmpDbg comment + /// + public static class OpenApiReferencableExtensions + { + /// + /// TODO: tmpDbg comment + /// + /// Element to validate + /// Optional set of rules to use for validation + /// An IEnumerable of errors. This function will never return null. + public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, string jsonPointer) + { + throw new NotImplementedException(); + } + } +} From b7c37fc376f8cbaa3e24d16b39d4c98caf726a20 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Sun, 8 Nov 2020 15:34:37 +0000 Subject: [PATCH 15/32] Referencable elements can resolve a reference with an empty JSON pointer to themselves --- .../OpenApiReferencableExtensions.cs | 2 +- .../Workspaces/OpenApiReferencableTests.cs | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index 941d9a5c9..ff6fe61a9 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -23,7 +23,7 @@ public static class OpenApiReferencableExtensions /// An IEnumerable of errors. This function will never return null. public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, string jsonPointer) { - throw new NotImplementedException(); + return element; } } } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs new file mode 100644 index 000000000..7b02bf156 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using Microsoft.OpenApi.Extensions; +using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Models; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Workspaces +{ + + public class OpenApiReferencableTests + { + public static IEnumerable ReferencableElementResolvesEmptyJsonPointerToItselfTestData => + new List + { + new object[] { new OpenApiCallback() }, + new object[] { new OpenApiExample() }, + new object[] { new OpenApiHeader() }, + new object[] { new OpenApiLink() }, + new object[] { new OpenApiParameter() }, + new object[] { new OpenApiRequestBody() }, + new object[] { new OpenApiResponse() }, + new object[] { new OpenApiSchema() }, + new object[] { new OpenApiSecurityScheme() }, + new object[] { new OpenApiTag() } + + }; + + [Theory] + [MemberData(nameof(ReferencableElementResolvesEmptyJsonPointerToItselfTestData))] + public void ReferencableElementResolvesEmptyJsonPointerToItself(IOpenApiReferenceable referencableElement) + { + // Arrange - above + + // Act + var resolvedReference = referencableElement.ResolveReference(string.Empty); + + // Assert + Assert.Same(referencableElement, resolvedReference); + } + } +} From afcbce94e7d9de2c42da5147fd5cf4dacd10dbb8 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Sun, 8 Nov 2020 16:06:22 +0000 Subject: [PATCH 16/32] Resolve reference to 'schema' property on Parameter element --- .../OpenApiReferencableExtensions.cs | 26 +++++++++++++- .../Workspaces/OpenApiReferencableTests.cs | 35 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index ff6fe61a9..2c12290d5 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -23,7 +24,30 @@ public static class OpenApiReferencableExtensions /// An IEnumerable of errors. This function will never return null. public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, string jsonPointer) { - return element; + if (jsonPointer == string.Empty) + return element; + if (element.GetType() == typeof(OpenApiParameter)) + { + return ResolveReferenceOnParameterElement((OpenApiParameter)element, jsonPointer); + } + throw new NotImplementedException(); + } + + private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiParameter parameterElement, string jsonPointer) + { + var jsonPointerTokens = jsonPointer.Split('/'); + switch (jsonPointerTokens.First()) + { + case OpenApiConstants.Schema: + return parameterElement.Schema; + case OpenApiConstants.Examples: + { + var mapKey = jsonPointerTokens.ElementAt(1); + return parameterElement.Examples[mapKey]; + } + default: + throw new NotImplementedException(); + } } } } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs index 7b02bf156..0b3dfa400 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -40,5 +40,40 @@ public void ReferencableElementResolvesEmptyJsonPointerToItself(IOpenApiReferenc // Assert Assert.Same(referencableElement, resolvedReference); } + + [Fact] + public void ParameterElementCanResolveReferenceToSchemaProperty() + { + // Arrange + var parameterElement = new OpenApiParameter + { + Schema = new OpenApiSchema() + }; + + // Act + var resolvedReference = parameterElement.ResolveReference("schema"); + + // Assert + Assert.Same(parameterElement.Schema, resolvedReference); + } + + [Fact] + public void ParameterElementCanResolveReferenceToExampleTmpDbgImproveMyName() + { + // Arrange + var parameterElement = new OpenApiParameter + { + Examples = new Dictionary() + { + { "example1", new OpenApiExample() } + }, + }; + + // Act + var resolvedReference = parameterElement.ResolveReference("examples/example1"); + + // Assert + Assert.Same(parameterElement.Examples["example1"], resolvedReference); + } } } From 6c696056c1ed95ea53b0bb505f39bd4e0564f7c5 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Sun, 8 Nov 2020 17:18:37 +0000 Subject: [PATCH 17/32] Throw exception when we try to reference a type that cannot be referenced on OpenApiParameter --- .../OpenApiReferencableExtensions.cs | 22 +++++++++--- .../Workspaces/OpenApiReferencableTests.cs | 34 +++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index 2c12290d5..bb9375f45 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; using Microsoft.OpenApi.Services; using Microsoft.OpenApi.Validations; @@ -26,9 +28,16 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable { if (jsonPointer == string.Empty) return element; - if (element.GetType() == typeof(OpenApiParameter)) + try { - return ResolveReferenceOnParameterElement((OpenApiParameter)element, jsonPointer); + if (element.GetType() == typeof(OpenApiParameter)) + { + return ResolveReferenceOnParameterElement((OpenApiParameter)element, jsonPointer); + } + } + catch (KeyNotFoundException) + { + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } throw new NotImplementedException(); } @@ -36,17 +45,20 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiParameter parameterElement, string jsonPointer) { var jsonPointerTokens = jsonPointer.Split('/'); - switch (jsonPointerTokens.First()) + var propertyName = jsonPointerTokens.First(); + switch (propertyName) { case OpenApiConstants.Schema: return parameterElement.Schema; case OpenApiConstants.Examples: { - var mapKey = jsonPointerTokens.ElementAt(1); + var mapKey = jsonPointerTokens.ElementAtOrDefault(1); + if (!(jsonPointerTokens.Length >= 2 && parameterElement.Examples.ContainsKey(mapKey))) + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); return parameterElement.Examples[mapKey]; } default: - throw new NotImplementedException(); + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } } } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs index 0b3dfa400..f54afe796 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +using System; using System.Collections.Generic; +using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Properties; using Xunit; namespace Microsoft.OpenApi.Tests.Workspaces @@ -75,5 +78,36 @@ public void ParameterElementCanResolveReferenceToExampleTmpDbgImproveMyName() // Assert Assert.Same(parameterElement.Examples["example1"], resolvedReference); } + + public static IEnumerable ParameterElementShouldThrowOnInvalidReferenceIdTestData => + new List + { + new object[] { "/" }, + new object[] { "a" }, + new object[] { "examples" }, + new object[] { "examples/a" }, + + }; + + [Theory] + [MemberData(nameof(ParameterElementShouldThrowOnInvalidReferenceIdTestData))] + public void ParameterElementShouldThrowOnInvalidReferenceId(string jsonPointer) + { + // Arrange + var parameterElement = new OpenApiParameter + { + Examples = new Dictionary() + { + { "example1", new OpenApiExample() } + }, + }; + + // Act + Action resolveReference = () => parameterElement.ResolveReference(jsonPointer); + + // Assert + var exception = Assert.Throws(resolveReference); + Assert.Equal(String.Format(SRResource.InvalidReferenceId, jsonPointer), exception.Message); + } } } From 7db3ea70c96a4f163a6e2dd927aaf33fefa3b3a8 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Wed, 11 Nov 2020 18:14:52 +0000 Subject: [PATCH 18/32] Refactoring of OpenApiReferencable units; add parameterised tests replacing multiple existing tests --- .../OpenApiReferencableExtensions.cs | 8 +- .../Workspaces/OpenApiReferencableTests.cs | 107 +++++++----------- 2 files changed, 45 insertions(+), 70 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index bb9375f45..809090d76 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -26,7 +26,7 @@ public static class OpenApiReferencableExtensions /// An IEnumerable of errors. This function will never return null. public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, string jsonPointer) { - if (jsonPointer == string.Empty) + if (jsonPointer == "/") return element; try { @@ -45,16 +45,14 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiParameter parameterElement, string jsonPointer) { var jsonPointerTokens = jsonPointer.Split('/'); - var propertyName = jsonPointerTokens.First(); + var propertyName = jsonPointerTokens.ElementAtOrDefault(1); switch (propertyName) { case OpenApiConstants.Schema: return parameterElement.Schema; case OpenApiConstants.Examples: { - var mapKey = jsonPointerTokens.ElementAtOrDefault(1); - if (!(jsonPointerTokens.Length >= 2 && parameterElement.Examples.ContainsKey(mapKey))) - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + var mapKey = jsonPointerTokens.ElementAtOrDefault(2); return parameterElement.Examples[mapKey]; } default: diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs index f54afe796..cb54e9da4 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -15,76 +15,62 @@ namespace Microsoft.OpenApi.Tests.Workspaces public class OpenApiReferencableTests { - public static IEnumerable ReferencableElementResolvesEmptyJsonPointerToItselfTestData => - new List + private static readonly OpenApiCallback _callbackFragment = new OpenApiCallback(); + private static readonly OpenApiExample _exampleFragment = new OpenApiExample(); + private static readonly OpenApiLink _linkFragment = new OpenApiLink(); + private static readonly OpenApiHeader _headerFragment = new OpenApiHeader(); + private static readonly OpenApiParameter _parameterFragment = new OpenApiParameter { - new object[] { new OpenApiCallback() }, - new object[] { new OpenApiExample() }, - new object[] { new OpenApiHeader() }, - new object[] { new OpenApiLink() }, - new object[] { new OpenApiParameter() }, - new object[] { new OpenApiRequestBody() }, - new object[] { new OpenApiResponse() }, - new object[] { new OpenApiSchema() }, - new object[] { new OpenApiSecurityScheme() }, - new object[] { new OpenApiTag() } - + Schema = new OpenApiSchema(), + Examples = new Dictionary + { + { "example1", new OpenApiExample() } + } }; + private static readonly OpenApiRequestBody _requestBodyFragment = new OpenApiRequestBody(); + private static readonly OpenApiResponse _responseFragment = new OpenApiResponse(); + private static readonly OpenApiSchema _schemaFragment = new OpenApiSchema(); + private static readonly OpenApiSecurityScheme _securitySchemeFragment = new OpenApiSecurityScheme(); + private static readonly OpenApiTag _tagFragment = new OpenApiTag(); - [Theory] - [MemberData(nameof(ReferencableElementResolvesEmptyJsonPointerToItselfTestData))] - public void ReferencableElementResolvesEmptyJsonPointerToItself(IOpenApiReferenceable referencableElement) - { - // Arrange - above - - // Act - var resolvedReference = referencableElement.ResolveReference(string.Empty); - - // Assert - Assert.Same(referencableElement, resolvedReference); - } - - [Fact] - public void ParameterElementCanResolveReferenceToSchemaProperty() + public static IEnumerable ReferencableElementsCanResolveReferencesTestData => + new List { - // Arrange - var parameterElement = new OpenApiParameter - { - Schema = new OpenApiSchema() - }; - - // Act - var resolvedReference = parameterElement.ResolveReference("schema"); - - // Assert - Assert.Same(parameterElement.Schema, resolvedReference); - } + new object[] { _callbackFragment, "/", _callbackFragment }, + new object[] { _exampleFragment, "/", _exampleFragment }, + new object[] { _linkFragment, "/", _linkFragment }, + new object[] { _headerFragment, "/", _headerFragment }, + new object[] { _parameterFragment, "/", _parameterFragment }, + new object[] { _parameterFragment, "/schema", _parameterFragment.Schema }, + new object[] { _parameterFragment, "/examples/example1", _parameterFragment.Examples["example1"] }, + new object[] { _requestBodyFragment, "/", _requestBodyFragment }, + new object[] { _responseFragment, "/", _responseFragment }, + new object[] { _schemaFragment, "/", _schemaFragment}, + new object[] { _securitySchemeFragment, "/", _securitySchemeFragment}, + new object[] { _tagFragment, "/", _tagFragment}, + }; - [Fact] - public void ParameterElementCanResolveReferenceToExampleTmpDbgImproveMyName() + [Theory] + [MemberData(nameof(ReferencableElementsCanResolveReferencesTestData))] + public void ReferencableElementsCanResolveReferences( + IOpenApiReferenceable element, + string pointer, + IOpenApiElement expectedResolvedElement) { - // Arrange - var parameterElement = new OpenApiParameter - { - Examples = new Dictionary() - { - { "example1", new OpenApiExample() } - }, - }; - // Act - var resolvedReference = parameterElement.ResolveReference("examples/example1"); + var actualResolvedElement = element.ResolveReference(pointer); // Assert - Assert.Same(parameterElement.Examples["example1"], resolvedReference); + Assert.Same(expectedResolvedElement, actualResolvedElement); } public static IEnumerable ParameterElementShouldThrowOnInvalidReferenceIdTestData => new List { - new object[] { "/" }, + new object[] { "" }, new object[] { "a" }, new object[] { "examples" }, + new object[] { "examples/" }, new object[] { "examples/a" }, }; @@ -93,21 +79,12 @@ public void ParameterElementCanResolveReferenceToExampleTmpDbgImproveMyName() [MemberData(nameof(ParameterElementShouldThrowOnInvalidReferenceIdTestData))] public void ParameterElementShouldThrowOnInvalidReferenceId(string jsonPointer) { - // Arrange - var parameterElement = new OpenApiParameter - { - Examples = new Dictionary() - { - { "example1", new OpenApiExample() } - }, - }; - // Act - Action resolveReference = () => parameterElement.ResolveReference(jsonPointer); + Action resolveReference = () => _parameterFragment.ResolveReference(jsonPointer); // Assert var exception = Assert.Throws(resolveReference); - Assert.Equal(String.Format(SRResource.InvalidReferenceId, jsonPointer), exception.Message); + Assert.Equal(string.Format(SRResource.InvalidReferenceId, jsonPointer), exception.Message); } } } From 4eb1c6c1ae576785e12f80c531868ad97942c424 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Wed, 11 Nov 2020 18:21:48 +0000 Subject: [PATCH 19/32] Add resolving references to child properties of a Header fragment --- .../OpenApiReferencableExtensions.cs | 22 +++++++++++++++++++ .../Workspaces/OpenApiReferencableTests.cs | 11 +++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index 809090d76..5c4423de3 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -30,6 +30,10 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable return element; try { + if (element.GetType() == typeof(OpenApiHeader)) + { + return ResolveReferenceOnHeaderElement((OpenApiHeader)element, jsonPointer); + } if (element.GetType() == typeof(OpenApiParameter)) { return ResolveReferenceOnParameterElement((OpenApiParameter)element, jsonPointer); @@ -42,6 +46,24 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable throw new NotImplementedException(); } + private static IOpenApiReferenceable ResolveReferenceOnHeaderElement(OpenApiHeader headerElement, string jsonPointer) + { + var jsonPointerTokens = jsonPointer.Split('/'); + var propertyName = jsonPointerTokens.ElementAtOrDefault(1); + switch (propertyName) + { + case OpenApiConstants.Schema: + return headerElement.Schema; + case OpenApiConstants.Examples: + { + var mapKey = jsonPointerTokens.ElementAtOrDefault(2); + return headerElement.Examples[mapKey]; + } + default: + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + } + } + private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiParameter parameterElement, string jsonPointer) { var jsonPointerTokens = jsonPointer.Split('/'); diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs index cb54e9da4..b174462c4 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -18,7 +18,14 @@ public class OpenApiReferencableTests private static readonly OpenApiCallback _callbackFragment = new OpenApiCallback(); private static readonly OpenApiExample _exampleFragment = new OpenApiExample(); private static readonly OpenApiLink _linkFragment = new OpenApiLink(); - private static readonly OpenApiHeader _headerFragment = new OpenApiHeader(); + private static readonly OpenApiHeader _headerFragment = new OpenApiHeader() + { + Schema = new OpenApiSchema(), + Examples = new Dictionary + { + { "example1", new OpenApiExample() } + } + }; private static readonly OpenApiParameter _parameterFragment = new OpenApiParameter { Schema = new OpenApiSchema(), @@ -40,6 +47,8 @@ public class OpenApiReferencableTests new object[] { _exampleFragment, "/", _exampleFragment }, new object[] { _linkFragment, "/", _linkFragment }, new object[] { _headerFragment, "/", _headerFragment }, + new object[] { _headerFragment, "/schema", _headerFragment.Schema }, + new object[] { _headerFragment, "/examples/example1", _headerFragment.Examples["example1"] }, new object[] { _parameterFragment, "/", _parameterFragment }, new object[] { _parameterFragment, "/schema", _parameterFragment.Schema }, new object[] { _parameterFragment, "/examples/example1", _parameterFragment.Examples["example1"] }, From 4b50cc5d7f52cdfe743742d9aa9f9731cd1587bc Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Wed, 11 Nov 2020 18:48:06 +0000 Subject: [PATCH 20/32] Further unit tests covering ResolveReference method on IOpenApiReferencable interface --- .../OpenApiReferencableExtensions.cs | 14 ++++++- .../Workspaces/OpenApiReferencableTests.cs | 38 ++++++++++++------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index 5c4423de3..e4364ed4e 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -43,11 +43,15 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable { throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } - throw new NotImplementedException(); + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } private static IOpenApiReferenceable ResolveReferenceOnHeaderElement(OpenApiHeader headerElement, string jsonPointer) { + if (string.IsNullOrEmpty(jsonPointer)) + { + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + } var jsonPointerTokens = jsonPointer.Split('/'); var propertyName = jsonPointerTokens.ElementAtOrDefault(1); switch (propertyName) @@ -56,6 +60,8 @@ private static IOpenApiReferenceable ResolveReferenceOnHeaderElement(OpenApiHead return headerElement.Schema; case OpenApiConstants.Examples: { + if (jsonPointerTokens.Length < 3) + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); var mapKey = jsonPointerTokens.ElementAtOrDefault(2); return headerElement.Examples[mapKey]; } @@ -66,6 +72,10 @@ private static IOpenApiReferenceable ResolveReferenceOnHeaderElement(OpenApiHead private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiParameter parameterElement, string jsonPointer) { + if (string.IsNullOrEmpty(jsonPointer)) + { + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + } var jsonPointerTokens = jsonPointer.Split('/'); var propertyName = jsonPointerTokens.ElementAtOrDefault(1); switch (propertyName) @@ -74,6 +84,8 @@ private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiP return parameterElement.Schema; case OpenApiConstants.Examples: { + if (jsonPointerTokens.Length < 3) + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); var mapKey = jsonPointerTokens.ElementAtOrDefault(2); return parameterElement.Examples[mapKey]; } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs index b174462c4..a15c1c458 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -40,7 +40,7 @@ public class OpenApiReferencableTests private static readonly OpenApiSecurityScheme _securitySchemeFragment = new OpenApiSecurityScheme(); private static readonly OpenApiTag _tagFragment = new OpenApiTag(); - public static IEnumerable ReferencableElementsCanResolveReferencesTestData => + public static IEnumerable ResolveReferenceCanResolveValidJsonPointersTestData => new List { new object[] { _callbackFragment, "/", _callbackFragment }, @@ -60,36 +60,46 @@ public class OpenApiReferencableTests }; [Theory] - [MemberData(nameof(ReferencableElementsCanResolveReferencesTestData))] - public void ReferencableElementsCanResolveReferences( + [MemberData(nameof(ResolveReferenceCanResolveValidJsonPointersTestData))] + public void ResolveReferenceCanResolveValidJsonPointers( IOpenApiReferenceable element, - string pointer, + string jsonPointer, IOpenApiElement expectedResolvedElement) { // Act - var actualResolvedElement = element.ResolveReference(pointer); + var actualResolvedElement = element.ResolveReference(jsonPointer); // Assert Assert.Same(expectedResolvedElement, actualResolvedElement); } - public static IEnumerable ParameterElementShouldThrowOnInvalidReferenceIdTestData => + public static IEnumerable ResolveReferenceShouldThrowOnInvalidReferenceIdTestData => new List { - new object[] { "" }, - new object[] { "a" }, - new object[] { "examples" }, - new object[] { "examples/" }, - new object[] { "examples/a" }, + new object[] { _callbackFragment, null }, + new object[] { _callbackFragment, "" }, + new object[] { _callbackFragment, "/a" }, + new object[] { _headerFragment, null }, + new object[] { _headerFragment, "" }, + new object[] { _headerFragment, "/a" }, + new object[] { _headerFragment, "/examples" }, + new object[] { _headerFragment, "/examples/" }, + new object[] { _headerFragment, "/examples/a" }, + new object[] { _parameterFragment, null }, + new object[] { _parameterFragment, "" }, + new object[] { _parameterFragment, "/a" }, + new object[] { _parameterFragment, "/examples" }, + new object[] { _parameterFragment, "/examples/" }, + new object[] { _parameterFragment, "/examples/a" } }; [Theory] - [MemberData(nameof(ParameterElementShouldThrowOnInvalidReferenceIdTestData))] - public void ParameterElementShouldThrowOnInvalidReferenceId(string jsonPointer) + [MemberData(nameof(ResolveReferenceShouldThrowOnInvalidReferenceIdTestData))] + public void ResolveReferenceShouldThrowOnInvalidReferenceId(IOpenApiReferenceable element, string jsonPointer) { // Act - Action resolveReference = () => _parameterFragment.ResolveReference(jsonPointer); + Action resolveReference = () => element.ResolveReference(jsonPointer); // Assert var exception = Assert.Throws(resolveReference); From c86fd89af8ef645d704a5c34a1c3c7aec6ea3a56 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Wed, 11 Nov 2020 19:00:40 +0000 Subject: [PATCH 21/32] Resolve references to child properties of Response fragments --- .../OpenApiReferencableExtensions.cs | 33 +++++++++++++++++++ .../Workspaces/OpenApiReferencableTests.cs | 25 ++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index e4364ed4e..ad880258c 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -38,6 +38,10 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable { return ResolveReferenceOnParameterElement((OpenApiParameter)element, jsonPointer); } + if (element.GetType() == typeof(OpenApiResponse)) + { + return ResolveReferenceOnResponseElement((OpenApiResponse)element, jsonPointer); + } } catch (KeyNotFoundException) { @@ -93,5 +97,34 @@ private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiP throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } } + + private static IOpenApiReferenceable ResolveReferenceOnResponseElement(OpenApiResponse responseElement, string jsonPointer) + { + if (string.IsNullOrEmpty(jsonPointer)) + { + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + } + var jsonPointerTokens = jsonPointer.Split('/'); + var propertyName = jsonPointerTokens.ElementAtOrDefault(1); + switch (propertyName) + { + case OpenApiConstants.Headers: + { + if (jsonPointerTokens.Length < 3) + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + var mapKey = jsonPointerTokens.ElementAtOrDefault(2); + return responseElement.Headers[mapKey]; + } + case OpenApiConstants.Links: + { + if (jsonPointerTokens.Length < 3) + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + var mapKey = jsonPointerTokens.ElementAtOrDefault(2); + return responseElement.Links[mapKey]; + } + default: + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + } + } } } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs index a15c1c458..469bbbbb3 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -35,7 +35,17 @@ public class OpenApiReferencableTests } }; private static readonly OpenApiRequestBody _requestBodyFragment = new OpenApiRequestBody(); - private static readonly OpenApiResponse _responseFragment = new OpenApiResponse(); + private static readonly OpenApiResponse _responseFragment = new OpenApiResponse() + { + Headers = new Dictionary + { + { "header1", new OpenApiHeader() } + }, + Links = new Dictionary + { + { "link1", new OpenApiLink() } + } + }; private static readonly OpenApiSchema _schemaFragment = new OpenApiSchema(); private static readonly OpenApiSecurityScheme _securitySchemeFragment = new OpenApiSecurityScheme(); private static readonly OpenApiTag _tagFragment = new OpenApiTag(); @@ -54,6 +64,8 @@ public class OpenApiReferencableTests new object[] { _parameterFragment, "/examples/example1", _parameterFragment.Examples["example1"] }, new object[] { _requestBodyFragment, "/", _requestBodyFragment }, new object[] { _responseFragment, "/", _responseFragment }, + new object[] { _responseFragment, "/headers/header1", _responseFragment.Headers["header1"] }, + new object[] { _responseFragment, "/links/link1", _responseFragment.Links["link1"] }, new object[] { _schemaFragment, "/", _schemaFragment}, new object[] { _securitySchemeFragment, "/", _securitySchemeFragment}, new object[] { _tagFragment, "/", _tagFragment}, @@ -90,7 +102,16 @@ public void ResolveReferenceCanResolveValidJsonPointers( new object[] { _parameterFragment, "/a" }, new object[] { _parameterFragment, "/examples" }, new object[] { _parameterFragment, "/examples/" }, - new object[] { _parameterFragment, "/examples/a" } + new object[] { _parameterFragment, "/examples/a" }, + new object[] { _responseFragment, null }, + new object[] { _responseFragment, "" }, + new object[] { _responseFragment, "/a" }, + new object[] { _responseFragment, "/headers" }, + new object[] { _responseFragment, "/headers/" }, + new object[] { _responseFragment, "/headers/a" }, + new object[] { _responseFragment, "/content" }, + new object[] { _responseFragment, "/content/" }, + new object[] { _responseFragment, "/content/a" }, }; From 23c8417f4ac56ad8750090139a7f4d2f20a9236d Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Sat, 5 Sep 2020 12:58:00 +0100 Subject: [PATCH 22/32] Update implementation of OpenApiWorkspace.ResolveReference to support referencing document fragments --- .../Services/OpenApiWorkspace.cs | 20 +++------ .../Workspaces/OpenApiWorkspaceTests.cs | 44 +++++++++++++++++++ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 637187051..d694b7fb1 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; @@ -18,7 +19,7 @@ namespace Microsoft.OpenApi.Services public class OpenApiWorkspace { private Dictionary _documents = new Dictionary(); - private Dictionary _fragments = new Dictionary(); + private Dictionary _fragments = new Dictionary(); private Dictionary _artifacts = new Dictionary(); /// @@ -92,7 +93,7 @@ public void AddDocument(string location, OpenApiDocument document) /// Not sure how this is going to work. Does the reference just point to the fragment as a whole, or do we need to /// to be able to point into the fragment. Keeping it private until we figure it out. /// - private void AddFragment(string location, IOpenApiElement fragment) + public void AddFragment(string location, IOpenApiReferenceable fragment) { _fragments.Add(ToLocationUrl(location), fragment); } @@ -114,21 +115,14 @@ public void AddArtifact(string location, Stream artifact) /// public IOpenApiReferenceable ResolveReference(OpenApiReference reference) { - if (_documents.TryGetValue(new Uri(BaseUrl,reference.ExternalResource),out var doc)) + if (_documents.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var doc)) { - return doc.ResolveReference(reference, true); + return doc.ResolveReference(reference, true); } else if (_fragments.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var fragment)) { - var frag = fragment as IOpenApiReferenceable; - if (frag != null) - { - return null; // frag.ResolveReference(reference, true); // IOpenApiElement needs to implement ResolveReference - } - else - { - return null; - } + var jsonPointer = $"/{reference.Id ?? string.Empty}"; + return fragment.ResolveReference(jsonPointer); } return null; } diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs index 47002ace2..b82327a5d 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiWorkspaceTests.cs @@ -155,6 +155,50 @@ public void OpenApiWorkspacesShouldLoadDocumentFragments() Assert.True(false); } + [Fact] + public void OpenApiWorkspacesCanResolveReferencesToDocumentFragments() + { + // Arrange + var workspace = new OpenApiWorkspace(); + var schemaFragment = new OpenApiSchema { Type = "string", Description = "Schema from a fragment" }; + workspace.AddFragment("fragment", schemaFragment); + + // Act + var schema = workspace.ResolveReference(new OpenApiReference() + { + ExternalResource = "fragment" + }) as OpenApiSchema; + + // Assert + Assert.NotNull(schema); + Assert.Equal("Schema from a fragment", schema.Description); + } + + [Fact] + public void OpenApiWorkspacesCanResolveReferencesToDocumentFragmentsWithJsonPointers() + { + // Arrange + var workspace = new OpenApiWorkspace(); + var responseFragment = new OpenApiResponse() + { + Headers = new Dictionary + { + { "header1", new OpenApiHeader() } + } + }; + workspace.AddFragment("fragment", responseFragment); + + // Act + var resolvedElement = workspace.ResolveReference(new OpenApiReference() + { + Id = "headers/header1", + ExternalResource = "fragment" + }); + + // Assert + Assert.Same(responseFragment.Headers["header1"], resolvedElement); + } + // Test artifacts From 56f45245dc00e25ba2bf6b08fa09dd53e72f4262 Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Wed, 11 Nov 2020 19:23:40 +0000 Subject: [PATCH 23/32] Improve comments in OpenApiReferencableExtensions --- .../Extensions/OpenApiReferencableExtensions.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index ad880258c..b08710407 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -8,22 +8,20 @@ using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Properties; -using Microsoft.OpenApi.Services; -using Microsoft.OpenApi.Validations; namespace Microsoft.OpenApi.Extensions { /// - /// TODO: tmpDbg comment + /// Extension methods for resolving references on elements. /// public static class OpenApiReferencableExtensions { /// /// TODO: tmpDbg comment /// - /// Element to validate - /// Optional set of rules to use for validation - /// An IEnumerable of errors. This function will never return null. + /// The referencable Open API element on which to apply the JSON pointer + /// a JSON Pointer [RFC 6901](https://tools.ietf.org/html/rfc6901). + /// The element pointed to by the JSON pointer. public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, string jsonPointer) { if (jsonPointer == "/") From e3ca80ee842fd0b4d12b67fb36e98337a6bc6fdd Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Thu, 12 Nov 2020 18:30:07 +0000 Subject: [PATCH 24/32] Refactoring in OpenApiReferencableExtensions to reduce duplicated code --- .../OpenApiReferencableExtensions.cs | 88 ++++++++----------- 1 file changed, 35 insertions(+), 53 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index b08710407..2f90cdad2 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System; using System.Collections.Generic; using System.Linq; using Microsoft.OpenApi.Exceptions; @@ -25,20 +24,29 @@ public static class OpenApiReferencableExtensions public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, string jsonPointer) { if (jsonPointer == "/") + { return element; + } + if (string.IsNullOrEmpty(jsonPointer)) + { + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + } + var jsonPointerTokens = jsonPointer.Split('/'); + var propertyName = jsonPointerTokens.ElementAtOrDefault(1); + var mapKey = jsonPointerTokens.ElementAtOrDefault(2); try { if (element.GetType() == typeof(OpenApiHeader)) { - return ResolveReferenceOnHeaderElement((OpenApiHeader)element, jsonPointer); + return ResolveReferenceOnHeaderElement((OpenApiHeader)element, propertyName, mapKey, jsonPointer); } if (element.GetType() == typeof(OpenApiParameter)) { - return ResolveReferenceOnParameterElement((OpenApiParameter)element, jsonPointer); + return ResolveReferenceOnParameterElement((OpenApiParameter)element, propertyName, mapKey, jsonPointer); } if (element.GetType() == typeof(OpenApiResponse)) { - return ResolveReferenceOnResponseElement((OpenApiResponse)element, jsonPointer); + return ResolveReferenceOnResponseElement((OpenApiResponse)element, propertyName, mapKey, jsonPointer); } } catch (KeyNotFoundException) @@ -48,78 +56,52 @@ public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } - private static IOpenApiReferenceable ResolveReferenceOnHeaderElement(OpenApiHeader headerElement, string jsonPointer) + private static IOpenApiReferenceable ResolveReferenceOnHeaderElement( + OpenApiHeader headerElement, + string propertyName, + string mapKey, + string jsonPointer) { - if (string.IsNullOrEmpty(jsonPointer)) - { - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - } - var jsonPointerTokens = jsonPointer.Split('/'); - var propertyName = jsonPointerTokens.ElementAtOrDefault(1); switch (propertyName) { case OpenApiConstants.Schema: return headerElement.Schema; - case OpenApiConstants.Examples: - { - if (jsonPointerTokens.Length < 3) - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - var mapKey = jsonPointerTokens.ElementAtOrDefault(2); - return headerElement.Examples[mapKey]; - } + case OpenApiConstants.Examples when mapKey != null: + return headerElement.Examples[mapKey]; default: throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } } - private static IOpenApiReferenceable ResolveReferenceOnParameterElement(OpenApiParameter parameterElement, string jsonPointer) + private static IOpenApiReferenceable ResolveReferenceOnParameterElement( + OpenApiParameter parameterElement, + string propertyName, + string mapKey, + string jsonPointer) { - if (string.IsNullOrEmpty(jsonPointer)) - { - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - } - var jsonPointerTokens = jsonPointer.Split('/'); - var propertyName = jsonPointerTokens.ElementAtOrDefault(1); switch (propertyName) { case OpenApiConstants.Schema: return parameterElement.Schema; - case OpenApiConstants.Examples: - { - if (jsonPointerTokens.Length < 3) - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - var mapKey = jsonPointerTokens.ElementAtOrDefault(2); - return parameterElement.Examples[mapKey]; - } + case OpenApiConstants.Examples when mapKey != null: + return parameterElement.Examples[mapKey]; default: throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } } - private static IOpenApiReferenceable ResolveReferenceOnResponseElement(OpenApiResponse responseElement, string jsonPointer) + private static IOpenApiReferenceable ResolveReferenceOnResponseElement( + OpenApiResponse responseElement, + string propertyName, + string mapKey, + string jsonPointer) { - if (string.IsNullOrEmpty(jsonPointer)) - { - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - } - var jsonPointerTokens = jsonPointer.Split('/'); - var propertyName = jsonPointerTokens.ElementAtOrDefault(1); switch (propertyName) { - case OpenApiConstants.Headers: - { - if (jsonPointerTokens.Length < 3) - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - var mapKey = jsonPointerTokens.ElementAtOrDefault(2); - return responseElement.Headers[mapKey]; - } - case OpenApiConstants.Links: - { - if (jsonPointerTokens.Length < 3) - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - var mapKey = jsonPointerTokens.ElementAtOrDefault(2); - return responseElement.Links[mapKey]; - } + case OpenApiConstants.Headers when mapKey != null: + return responseElement.Headers[mapKey]; + case OpenApiConstants.Links when mapKey != null: + return responseElement.Links[mapKey]; default: throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); } From dda0fe4de39befe357b3a70fbee9d58085cd0d6d Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Fri, 13 Nov 2020 18:38:19 +0000 Subject: [PATCH 25/32] Change IOpenApiReferenceable.ResolveReference method to using the JsonPointer class rather than a string for the JSON pointer argument --- .../OpenApiReferencableExtensions.cs | 39 ++++++++----------- src/Microsoft.OpenApi/JsonPointer.cs | 4 +- .../Services/OpenApiWorkspace.cs | 2 +- .../Workspaces/OpenApiReferencableTests.cs | 16 ++------ 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs index 2f90cdad2..11fcd7e9e 100644 --- a/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiReferencableExtensions.cs @@ -16,51 +16,46 @@ namespace Microsoft.OpenApi.Extensions public static class OpenApiReferencableExtensions { /// - /// TODO: tmpDbg comment + /// Resolves a JSON Pointer with respect to an element, returning the referenced element. /// /// The referencable Open API element on which to apply the JSON pointer - /// a JSON Pointer [RFC 6901](https://tools.ietf.org/html/rfc6901). + /// a JSON Pointer [RFC 6901](https://tools.ietf.org/html/rfc6901). /// The element pointed to by the JSON pointer. - public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, string jsonPointer) + public static IOpenApiReferenceable ResolveReference(this IOpenApiReferenceable element, JsonPointer pointer) { - if (jsonPointer == "/") + if (!pointer.Tokens.Any()) { return element; } - if (string.IsNullOrEmpty(jsonPointer)) - { - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); - } - var jsonPointerTokens = jsonPointer.Split('/'); - var propertyName = jsonPointerTokens.ElementAtOrDefault(1); - var mapKey = jsonPointerTokens.ElementAtOrDefault(2); + var propertyName = pointer.Tokens.FirstOrDefault(); + var mapKey = pointer.Tokens.ElementAtOrDefault(1); try { if (element.GetType() == typeof(OpenApiHeader)) { - return ResolveReferenceOnHeaderElement((OpenApiHeader)element, propertyName, mapKey, jsonPointer); + return ResolveReferenceOnHeaderElement((OpenApiHeader)element, propertyName, mapKey, pointer); } if (element.GetType() == typeof(OpenApiParameter)) { - return ResolveReferenceOnParameterElement((OpenApiParameter)element, propertyName, mapKey, jsonPointer); + return ResolveReferenceOnParameterElement((OpenApiParameter)element, propertyName, mapKey, pointer); } if (element.GetType() == typeof(OpenApiResponse)) { - return ResolveReferenceOnResponseElement((OpenApiResponse)element, propertyName, mapKey, jsonPointer); + return ResolveReferenceOnResponseElement((OpenApiResponse)element, propertyName, mapKey, pointer); } } catch (KeyNotFoundException) { - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer)); } - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer)); } private static IOpenApiReferenceable ResolveReferenceOnHeaderElement( OpenApiHeader headerElement, string propertyName, string mapKey, - string jsonPointer) + JsonPointer pointer) { switch (propertyName) { @@ -69,7 +64,7 @@ private static IOpenApiReferenceable ResolveReferenceOnHeaderElement( case OpenApiConstants.Examples when mapKey != null: return headerElement.Examples[mapKey]; default: - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer)); } } @@ -77,7 +72,7 @@ private static IOpenApiReferenceable ResolveReferenceOnParameterElement( OpenApiParameter parameterElement, string propertyName, string mapKey, - string jsonPointer) + JsonPointer pointer) { switch (propertyName) { @@ -86,7 +81,7 @@ private static IOpenApiReferenceable ResolveReferenceOnParameterElement( case OpenApiConstants.Examples when mapKey != null: return parameterElement.Examples[mapKey]; default: - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer)); } } @@ -94,7 +89,7 @@ private static IOpenApiReferenceable ResolveReferenceOnResponseElement( OpenApiResponse responseElement, string propertyName, string mapKey, - string jsonPointer) + JsonPointer pointer) { switch (propertyName) { @@ -103,7 +98,7 @@ private static IOpenApiReferenceable ResolveReferenceOnResponseElement( case OpenApiConstants.Links when mapKey != null: return responseElement.Links[mapKey]; default: - throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, jsonPointer)); + throw new OpenApiException(string.Format(SRResource.InvalidReferenceId, pointer)); } } } diff --git a/src/Microsoft.OpenApi/JsonPointer.cs b/src/Microsoft.OpenApi/JsonPointer.cs index 07e1305d6..c2aec5097 100644 --- a/src/Microsoft.OpenApi/JsonPointer.cs +++ b/src/Microsoft.OpenApi/JsonPointer.cs @@ -17,7 +17,9 @@ public class JsonPointer /// Pointer as string. public JsonPointer(string pointer) { - Tokens = pointer.Split('/').Skip(1).Select(Decode).ToArray(); + Tokens = string.IsNullOrEmpty(pointer) || pointer == "/" + ? new string[0] + : pointer.Split('/').Skip(1).Select(Decode).ToArray(); } /// diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index d694b7fb1..0b69a7168 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -121,7 +121,7 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference) } else if (_fragments.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var fragment)) { - var jsonPointer = $"/{reference.Id ?? string.Empty}"; + var jsonPointer = new JsonPointer($"/{reference.Id ?? string.Empty}"); return fragment.ResolveReference(jsonPointer); } return null; diff --git a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs index 469bbbbb3..2bae02b1f 100644 --- a/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs +++ b/test/Microsoft.OpenApi.Tests/Workspaces/OpenApiReferencableTests.cs @@ -68,7 +68,7 @@ public class OpenApiReferencableTests new object[] { _responseFragment, "/links/link1", _responseFragment.Links["link1"] }, new object[] { _schemaFragment, "/", _schemaFragment}, new object[] { _securitySchemeFragment, "/", _securitySchemeFragment}, - new object[] { _tagFragment, "/", _tagFragment}, + new object[] { _tagFragment, "/", _tagFragment} }; [Theory] @@ -79,7 +79,7 @@ public void ResolveReferenceCanResolveValidJsonPointers( IOpenApiElement expectedResolvedElement) { // Act - var actualResolvedElement = element.ResolveReference(jsonPointer); + var actualResolvedElement = element.ResolveReference(new JsonPointer(jsonPointer)); // Assert Assert.Same(expectedResolvedElement, actualResolvedElement); @@ -88,30 +88,22 @@ public void ResolveReferenceCanResolveValidJsonPointers( public static IEnumerable ResolveReferenceShouldThrowOnInvalidReferenceIdTestData => new List { - new object[] { _callbackFragment, null }, - new object[] { _callbackFragment, "" }, new object[] { _callbackFragment, "/a" }, - new object[] { _headerFragment, null }, - new object[] { _headerFragment, "" }, new object[] { _headerFragment, "/a" }, new object[] { _headerFragment, "/examples" }, new object[] { _headerFragment, "/examples/" }, new object[] { _headerFragment, "/examples/a" }, - new object[] { _parameterFragment, null }, - new object[] { _parameterFragment, "" }, new object[] { _parameterFragment, "/a" }, new object[] { _parameterFragment, "/examples" }, new object[] { _parameterFragment, "/examples/" }, new object[] { _parameterFragment, "/examples/a" }, - new object[] { _responseFragment, null }, - new object[] { _responseFragment, "" }, new object[] { _responseFragment, "/a" }, new object[] { _responseFragment, "/headers" }, new object[] { _responseFragment, "/headers/" }, new object[] { _responseFragment, "/headers/a" }, new object[] { _responseFragment, "/content" }, new object[] { _responseFragment, "/content/" }, - new object[] { _responseFragment, "/content/a" }, + new object[] { _responseFragment, "/content/a" } }; @@ -120,7 +112,7 @@ public void ResolveReferenceCanResolveValidJsonPointers( public void ResolveReferenceShouldThrowOnInvalidReferenceId(IOpenApiReferenceable element, string jsonPointer) { // Act - Action resolveReference = () => element.ResolveReference(jsonPointer); + Action resolveReference = () => element.ResolveReference(new JsonPointer(jsonPointer)); // Assert var exception = Assert.Throws(resolveReference); From 62b7e63dedd8be6a6d03a68e2abe834b0001a96c Mon Sep 17 00:00:00 2001 From: Simon Pearson Date: Fri, 13 Nov 2020 18:48:33 +0000 Subject: [PATCH 26/32] The OpenApiStreamReader.ReadFragment method may only read fragments that are externally referencable, i.e. implement IOpenApiReferenceable --- src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index 9dc14a7bd..eb36e9810 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -45,7 +45,7 @@ public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) /// Version of the OpenAPI specification that the fragment conforms to. /// Returns diagnostic object containing errors detected during parsing /// Instance of newly created OpenApiDocument - public T ReadFragment(Stream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiElement + public T ReadFragment(Stream input, OpenApiSpecVersion version, out OpenApiDiagnostic diagnostic) where T : IOpenApiReferenceable { using (var reader = new StreamReader(input)) { From 1debbf287fb431d54e0e9ca7be27a43c737e8a04 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 21 Dec 2020 16:53:54 -0500 Subject: [PATCH 27/32] Updated OpenApiWorkspaceLoader to support loading external references --- .../OpenApiReaderSettings.cs | 11 ++++++++ .../OpenApiYamlDocumentReader.cs | 9 +++++++ .../Services/OpenApiWorkspaceLoader.cs | 19 +++++++------ .../OpenApiWorkspaceStreamTests.cs | 27 ++++++++++++++++++- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs index 092699857..1f319b83c 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs @@ -3,10 +3,13 @@ using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; +using Microsoft.OpenApi.Readers.Interface; using Microsoft.OpenApi.Readers.ParseNodes; +using Microsoft.OpenApi.Readers.Services; using Microsoft.OpenApi.Validations; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -56,5 +59,13 @@ public class OpenApiReaderSettings /// URL where relative references should be resolved from if the description does not contain Server definitions /// public Uri BaseUrl { get; set; } + + /// + /// Function used to provide an alternative loader for accessing external references. + /// + /// + /// Default loader will attempt to dereference http(s) urls and file urls. + /// + public Func> CustomExternalLoader { get; set; } } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs index 215d7eefc..a4cafdd6a 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.IO; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; @@ -54,6 +55,14 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic switch (_settings.ReferenceResolution) { case ReferenceResolutionSetting.ResolveAllReferences: + var openApiWorkSpace = new OpenApiWorkspace(); + document.Workspace = openApiWorkSpace; + var streamLoader = new DefaultStreamLoader(); + + var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader.LoadAsync, _settings); + workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource="/" }, document); + + // TODO: Need to add ReadAsync for resolving all references. throw new ArgumentException(Properties.SRResource.CannotResolveRemoteReferencesSynchronously); case ReferenceResolutionSetting.ResolveLocalReferences: var errors = document.ResolveReferences(false); diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs index 42fffb523..bbf6898c3 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -7,21 +7,22 @@ using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.Interface; using Microsoft.OpenApi.Services; +using SharpYaml.Model; namespace Microsoft.OpenApi.Readers.Services { - internal class OpenApiWorkspaceLoader where TDiagnostic: IDiagnostic + internal class OpenApiWorkspaceLoader { private OpenApiWorkspace _workspace; - private IInputLoader _loader; - private TDiagnostic _diagnostics; - private IOpenApiReader _reader; + private Func> _loader; + private OpenApiDiagnostic _diagnostics; + private OpenApiReaderSettings _readerSettings; - public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, IInputLoader loader, IOpenApiReader reader) + public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, Func> loader, OpenApiReaderSettings readerSettings) { _workspace = workspace; _loader = loader; - _reader = reader; + _readerSettings = readerSettings; } internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument document) @@ -34,14 +35,16 @@ internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument docume var collectorWalker = new OpenApiWalker(referenceCollector); collectorWalker.Walk(document); + var reader = new OpenApiStreamReader(_readerSettings); + // Walk references foreach (var item in referenceCollector.References) { // If not already in workspace, load it and process references if (!_workspace.Contains(item.ExternalResource)) { - var input = await _loader.LoadAsync(new Uri(item.ExternalResource)); - var newDocument = _reader.Read(input, out _diagnostics); + var input = await _loader(new Uri(item.ExternalResource)); + var newDocument = reader.Read(input, out _diagnostics); // TODO merge _diagnositics await LoadAsync(item, newDocument); } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index 3893310c2..4cae0d2d7 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -1,12 +1,37 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Microsoft.OpenApi.Readers.Interface; +using Microsoft.OpenApi.Readers.Services; +using Microsoft.OpenApi.Services; +using Xunit; namespace Microsoft.OpenApi.Readers.Tests.OpenApiWorkspaceTests { - class OpenApiWorkspaceStreamTests + public class OpenApiWorkspaceStreamTests { + + + // Use OpenApiWorkspace to load a document and a referenced document + + //[Fact] + public void LoadDocumentIntoWorkspace() + { + // Create a reader that will resolve all references + var reader = new OpenApiStringReader(new OpenApiReaderSettings() { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + CustomExternalLoader = (url) => { return null; } + }); + + // Todo: this should be ReadAsync + var doc = reader.Read("", out OpenApiDiagnostic diagnostic); + + Assert.NotNull(doc.Workspace); + + } } } From d229688eb710c8f9b45f19dcb09059f6d7f21a6a Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 30 Jan 2021 12:02:44 -0500 Subject: [PATCH 28/32] Moved OpenAPIWorkspace to sync loading --- .../Interface/IStreamLoader.cs | 7 +++ .../OpenApiReaderSettings.cs | 2 +- .../OpenApiYamlDocumentReader.cs | 52 ++++++++++--------- .../Services/DefaultStreamLoader.cs | 16 +++++- .../Services/OpenApiWorkspaceLoader.cs | 10 ++-- 5 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs index 191f84761..180de8183 100644 --- a/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs @@ -19,5 +19,12 @@ public interface IInputLoader /// Identifier of some source of an OpenAPI Description /// A data objext that can be processed by a reader to generate an Task LoadAsync(Uri uri); + + /// + /// Use Uri to locate data and convert into an input object. + /// + /// + /// + TInput Load(Uri uri); } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs index 1f319b83c..1875796e6 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs @@ -66,6 +66,6 @@ public class OpenApiReaderSettings /// /// Default loader will attempt to dereference http(s) urls and file urls. /// - public Func> CustomExternalLoader { get; set; } + public Func CustomExternalLoader { get; set; } } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs index a4cafdd6a..768846abb 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -51,30 +51,7 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic // Parse the OpenAPI Document document = context.Parse(input); - // Resolve References if requested - switch (_settings.ReferenceResolution) - { - case ReferenceResolutionSetting.ResolveAllReferences: - var openApiWorkSpace = new OpenApiWorkspace(); - document.Workspace = openApiWorkSpace; - var streamLoader = new DefaultStreamLoader(); - - var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader.LoadAsync, _settings); - workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource="/" }, document); - - // TODO: Need to add ReadAsync for resolving all references. - throw new ArgumentException(Properties.SRResource.CannotResolveRemoteReferencesSynchronously); - case ReferenceResolutionSetting.ResolveLocalReferences: - var errors = document.ResolveReferences(false); - - foreach (var item in errors) - { - diagnostic.Errors.Add(item); - } - break; - case ReferenceResolutionSetting.DoNotResolveReferences: - break; - } + ResolveReferences(diagnostic, document); } catch (OpenApiException ex) { @@ -93,6 +70,33 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic return document; } + + private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument document) + { + // Resolve References if requested + switch (_settings.ReferenceResolution) + { + case ReferenceResolutionSetting.ResolveAllReferences: + var openApiWorkSpace = new OpenApiWorkspace(); + document.Workspace = openApiWorkSpace; + var streamLoader = new DefaultStreamLoader(); + + var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader.Load, _settings); + workspaceLoader.Load(new OpenApiReference() { ExternalResource = "/" }, document); + break; + case ReferenceResolutionSetting.ResolveLocalReferences: + var errors = document.ResolveReferences(false); + + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } + break; + case ReferenceResolutionSetting.DoNotResolveReferences: + break; + } + } + /// /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. /// diff --git a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs index 311642a82..f7ae3fa01 100644 --- a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs @@ -19,6 +19,21 @@ internal class DefaultStreamLoader : IInputLoader { private HttpClient _httpClient = new HttpClient(); + public Stream Load(Uri uri) + { + switch (uri.Scheme) + { + case "file": + return File.OpenRead(uri.AbsolutePath); + case "http": + case "https": + return _httpClient.GetStreamAsync(uri).GetAwaiter().GetResult(); + + default: + throw new ArgumentException("Unsupported scheme"); + } + } + public async Task LoadAsync(Uri uri) { switch (uri.Scheme) @@ -28,7 +43,6 @@ public async Task LoadAsync(Uri uri) case "http": case "https": return await _httpClient.GetStreamAsync(uri); - default: throw new ArgumentException("Unsupported scheme"); } diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs index bbf6898c3..01d16f505 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -14,18 +14,18 @@ namespace Microsoft.OpenApi.Readers.Services internal class OpenApiWorkspaceLoader { private OpenApiWorkspace _workspace; - private Func> _loader; + private Func _loader; private OpenApiDiagnostic _diagnostics; private OpenApiReaderSettings _readerSettings; - public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, Func> loader, OpenApiReaderSettings readerSettings) + public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, Func loader, OpenApiReaderSettings readerSettings) { _workspace = workspace; _loader = loader; _readerSettings = readerSettings; } - internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument document) + internal void Load(OpenApiReference reference, OpenApiDocument document) { _workspace.AddDocument(reference.ExternalResource, document); document.Workspace = _workspace; @@ -43,9 +43,9 @@ internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument docume // If not already in workspace, load it and process references if (!_workspace.Contains(item.ExternalResource)) { - var input = await _loader(new Uri(item.ExternalResource)); + var input = _loader(new Uri(item.ExternalResource)); var newDocument = reader.Read(input, out _diagnostics); // TODO merge _diagnositics - await LoadAsync(item, newDocument); + Load(item, newDocument); } } } From 65b70f2913897c76d6aa4a9c751359b9a3432306 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Mon, 22 Feb 2021 08:42:20 -0500 Subject: [PATCH 29/32] Enabled async read for workspace --- .../Interface/IStreamLoader.cs | 10 +-- .../OpenApiReaderSettings.cs | 2 +- .../OpenApiStreamReader.cs | 26 ++++++++ .../OpenApiTextReaderReader.cs | 26 ++++++++ .../OpenApiYamlDocumentReader.cs | 66 ++++++++++++++++++- src/Microsoft.OpenApi.Readers/ReadResult.cs | 18 +++++ .../Services/DefaultStreamLoader.cs | 2 +- .../Services/OpenApiWorkspaceLoader.cs | 12 ++-- .../OpenApiWorkspaceStreamTests.cs | 39 +++++++++-- 9 files changed, 179 insertions(+), 22 deletions(-) create mode 100644 src/Microsoft.OpenApi.Readers/ReadResult.cs diff --git a/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs index 180de8183..b93c69f39 100644 --- a/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Interface/IStreamLoader.cs @@ -2,29 +2,29 @@ // Licensed under the MIT license. using System; +using System.IO; using System.Threading.Tasks; using Microsoft.OpenApi.Models; namespace Microsoft.OpenApi.Readers.Interface { /// - /// Interface for service that translates a URI into an object that can be loaded by a Reader + /// Interface for service that translates a URI into a stream that can be loaded by a Reader /// - /// - public interface IInputLoader + public interface IStreamLoader { /// /// Use Uri to locate data and convert into an input object. /// /// Identifier of some source of an OpenAPI Description /// A data objext that can be processed by a reader to generate an - Task LoadAsync(Uri uri); + Task LoadAsync(Uri uri); /// /// Use Uri to locate data and convert into an input object. /// /// /// - TInput Load(Uri uri); + Stream Load(Uri uri); } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs index 1875796e6..da178ae86 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs @@ -66,6 +66,6 @@ public class OpenApiReaderSettings /// /// Default loader will attempt to dereference http(s) urls and file urls. /// - public Func CustomExternalLoader { get; set; } + public IStreamLoader CustomExternalLoader { get; set; } } } diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index eb36e9810..e448e538b 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.IO; +using System.Threading.Tasks; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.Interface; @@ -38,6 +39,31 @@ public OpenApiDocument Read(Stream input, out OpenApiDiagnostic diagnostic) } } + /// + /// Reads the stream input and parses it into an Open API document. + /// + /// Stream containing OpenAPI description to parse. + /// Instance result containing newly created OpenApiDocument and diagnostics object from the process + public async Task ReadAsync(Stream input) + { + MemoryStream bufferedStream; + if (input is MemoryStream) + { + bufferedStream = (MemoryStream)input; + } + else + { + // Buffer stream so that OpenApiTextReaderReader can process it synchronously + // YamlDocument doesn't support async reading. + bufferedStream = new MemoryStream(); + await input.CopyToAsync(bufferedStream); + } + + var reader = new StreamReader(bufferedStream); + + return await new OpenApiTextReaderReader(_settings).ReadAsync(reader); + } + /// /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. /// diff --git a/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs index c05456cec..f9510b257 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.Interface; @@ -51,6 +52,31 @@ public OpenApiDocument Read(TextReader input, out OpenApiDiagnostic diagnostic) return new OpenApiYamlDocumentReader(this._settings).Read(yamlDocument, out diagnostic); } + + public async Task ReadAsync(TextReader input) + { + YamlDocument yamlDocument; + + // Parse the YAML/JSON text in the TextReader into the YamlDocument + try + { + yamlDocument = LoadYamlDocument(input); + } + catch (YamlException ex) + { + var diagnostic = new OpenApiDiagnostic(); + diagnostic.Errors.Add(new OpenApiError($"#line={ex.Start.Line}", ex.Message)); + return new ReadResult + { + OpenApiDocument = null, + OpenApiDiagnostic = diagnostic + }; + } + + return await new OpenApiYamlDocumentReader(this._settings).ReadAsync(yamlDocument); + } + + /// /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. /// diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs index 768846abb..985ae7e55 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Threading.Tasks; using Microsoft.OpenApi.Exceptions; using Microsoft.OpenApi.Extensions; using Microsoft.OpenApi.Interfaces; @@ -71,7 +72,67 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic return document; } + public async Task ReadAsync(YamlDocument input) + { + var diagnostic = new OpenApiDiagnostic(); + var context = new ParsingContext(diagnostic) + { + ExtensionParsers = _settings.ExtensionParsers, + BaseUrl = _settings.BaseUrl + }; + + OpenApiDocument document = null; + try + { + // Parse the OpenAPI Document + document = context.Parse(input); + + await ResolveReferencesAsync(diagnostic, document); + } + catch (OpenApiException ex) + { + diagnostic.Errors.Add(new OpenApiError(ex)); + } + + // Validate the document + if (_settings.RuleSet != null && _settings.RuleSet.Rules.Count > 0) + { + var errors = document.Validate(_settings.RuleSet); + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } + } + + return new ReadResult() + { + OpenApiDocument = document, + OpenApiDiagnostic = diagnostic + }; + } + + private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument document) + { + // Resolve References if requested + switch (_settings.ReferenceResolution) + { + case ReferenceResolutionSetting.ResolveAllReferences: + throw new ArgumentException("Cannot resolve all references via a synchronous call. Use ReadAsync."); + case ReferenceResolutionSetting.ResolveLocalReferences: + var errors = document.ResolveReferences(false); + + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } + break; + case ReferenceResolutionSetting.DoNotResolveReferences: + break; + } + } + + private async Task ResolveReferencesAsync(OpenApiDiagnostic diagnostic, OpenApiDocument document) { // Resolve References if requested switch (_settings.ReferenceResolution) @@ -81,8 +142,8 @@ private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument doc document.Workspace = openApiWorkSpace; var streamLoader = new DefaultStreamLoader(); - var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader.Load, _settings); - workspaceLoader.Load(new OpenApiReference() { ExternalResource = "/" }, document); + var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader, _settings); + await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document); break; case ReferenceResolutionSetting.ResolveLocalReferences: var errors = document.ResolveReferences(false); @@ -97,6 +158,7 @@ private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument doc } } + /// /// Reads the stream input and parses the fragment of an OpenAPI description into an Open API Element. /// diff --git a/src/Microsoft.OpenApi.Readers/ReadResult.cs b/src/Microsoft.OpenApi.Readers/ReadResult.cs new file mode 100644 index 000000000..616c2f47c --- /dev/null +++ b/src/Microsoft.OpenApi.Readers/ReadResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; + +namespace Microsoft.OpenApi.Readers +{ + /// + /// + /// + public class ReadResult + { + public OpenApiDocument OpenApiDocument { set; get; } + public OpenApiDiagnostic OpenApiDiagnostic { set; get; } + } +} diff --git a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs index f7ae3fa01..865b43563 100644 --- a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs @@ -15,7 +15,7 @@ namespace Microsoft.OpenApi.Readers.Services /// /// Implementation of IInputLoader that loads streams from URIs /// - internal class DefaultStreamLoader : IInputLoader + internal class DefaultStreamLoader : IStreamLoader { private HttpClient _httpClient = new HttpClient(); diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs index 01d16f505..b40f815f4 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -14,18 +14,18 @@ namespace Microsoft.OpenApi.Readers.Services internal class OpenApiWorkspaceLoader { private OpenApiWorkspace _workspace; - private Func _loader; + private IStreamLoader _loader; private OpenApiDiagnostic _diagnostics; private OpenApiReaderSettings _readerSettings; - public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, Func loader, OpenApiReaderSettings readerSettings) + public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, IStreamLoader loader, OpenApiReaderSettings readerSettings) { _workspace = workspace; _loader = loader; _readerSettings = readerSettings; } - internal void Load(OpenApiReference reference, OpenApiDocument document) + internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument document) { _workspace.AddDocument(reference.ExternalResource, document); document.Workspace = _workspace; @@ -43,9 +43,9 @@ internal void Load(OpenApiReference reference, OpenApiDocument document) // If not already in workspace, load it and process references if (!_workspace.Contains(item.ExternalResource)) { - var input = _loader(new Uri(item.ExternalResource)); - var newDocument = reader.Read(input, out _diagnostics); // TODO merge _diagnositics - Load(item, newDocument); + var input = _loader.Load(new Uri(item.ExternalResource)); + var result = await reader.ReadAsync(input); // TODO merge _diagnositics + await LoadAsync(item, result.OpenApiDocument); } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index 4cae0d2d7..e37a8573d 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -18,20 +18,45 @@ public class OpenApiWorkspaceStreamTests // Use OpenApiWorkspace to load a document and a referenced document - //[Fact] - public void LoadDocumentIntoWorkspace() + [Fact] + public async Task LoadDocumentIntoWorkspace() { // Create a reader that will resolve all references - var reader = new OpenApiStringReader(new OpenApiReaderSettings() { - ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, - CustomExternalLoader = (url) => { return null; } + var reader = new OpenApiStreamReader(new OpenApiReaderSettings() + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + CustomExternalLoader = new MockLoader() }); // Todo: this should be ReadAsync - var doc = reader.Read("", out OpenApiDiagnostic diagnostic); + var stream = new MemoryStream(); + var doc = @"openapi: 3.0.0 +info: + title: foo + version: 1.0.0 +paths: {}"; + var wr = new StreamWriter(stream); + wr.Write(doc); + wr.Flush(); + stream.Position = 0; - Assert.NotNull(doc.Workspace); + var result = await reader.ReadAsync(stream); + Assert.NotNull(result.OpenApiDocument.Workspace); + + } + } + + public class MockLoader : IStreamLoader + { + public Stream Load(Uri uri) + { + return null; + } + + public async Task LoadAsync(Uri uri) + { + return null; } } } From 5090c01e8297b53beaff88aea3ae4ab1d56f39b5 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Tue, 2 Mar 2021 21:49:23 -0500 Subject: [PATCH 30/32] Support loading an OpenApiDocument with remote references. --- .../OpenApiStreamReader.cs | 1 + .../OpenApiTextReaderReader.cs | 5 ++ .../OpenApiYamlDocumentReader.cs | 25 +++++--- .../OpenApiRemoteReferenceCollector.cs | 63 ++----------------- .../Services/OpenApiWorkspaceLoader.cs | 2 +- .../V2/OpenApiV2VersionService.cs | 1 + .../V3/OpenApiV3VersionService.cs | 11 +++- .../Models/OpenApiDocument.cs | 12 ++-- .../Properties/SRResource.Designer.cs | 11 +++- .../Properties/SRResource.resx | 3 + .../Services/OpenApiWorkspace.cs | 2 +- .../Microsoft.OpenApi.Readers.Tests.csproj | 4 ++ .../OpenApiWorkspaceStreamTests.cs | 56 ++++++++++++++++- .../ConvertToOpenApiReferenceV3Tests.cs | 4 +- .../OpenApiWorkspace/TodoComponents.yaml | 25 ++++++++ .../Samples/OpenApiWorkspace/TodoMain.yaml | 16 +++++ .../WorkspaceTests.cs | 12 ++++ 17 files changed, 177 insertions(+), 76 deletions(-) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoComponents.yaml create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml create mode 100644 test/Microsoft.OpenApi.SmokeTests/WorkspaceTests.cs diff --git a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs index e448e538b..cab5d1a83 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiStreamReader.cs @@ -57,6 +57,7 @@ public async Task ReadAsync(Stream input) // YamlDocument doesn't support async reading. bufferedStream = new MemoryStream(); await input.CopyToAsync(bufferedStream); + bufferedStream.Position = 0; } var reader = new StreamReader(bufferedStream); diff --git a/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs index f9510b257..f4e81dee9 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiTextReaderReader.cs @@ -53,6 +53,11 @@ public OpenApiDocument Read(TextReader input, out OpenApiDiagnostic diagnostic) return new OpenApiYamlDocumentReader(this._settings).Read(yamlDocument, out diagnostic); } + /// + /// Reads the content of the TextReader. If there are references to external documents then they will be read asynchronously. + /// + /// TextReader containing OpenAPI description to parse. + /// A ReadResult instance that contains the resulting OpenApiDocument and a diagnostics instance. public async Task ReadAsync(TextReader input) { YamlDocument yamlDocument; diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs index 985ae7e55..bb00fb370 100644 --- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs +++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.OpenApi.Exceptions; @@ -134,28 +135,38 @@ private void ResolveReferences(OpenApiDiagnostic diagnostic, OpenApiDocument doc private async Task ResolveReferencesAsync(OpenApiDiagnostic diagnostic, OpenApiDocument document) { + List errors = new List(); + // Resolve References if requested switch (_settings.ReferenceResolution) { case ReferenceResolutionSetting.ResolveAllReferences: + + // Create workspace for all documents to live in. var openApiWorkSpace = new OpenApiWorkspace(); - document.Workspace = openApiWorkSpace; - var streamLoader = new DefaultStreamLoader(); + // Load this root document into the workspace + var streamLoader = new DefaultStreamLoader(); var workspaceLoader = new OpenApiWorkspaceLoader(openApiWorkSpace, _settings.CustomExternalLoader ?? streamLoader, _settings); await workspaceLoader.LoadAsync(new OpenApiReference() { ExternalResource = "/" }, document); - break; - case ReferenceResolutionSetting.ResolveLocalReferences: - var errors = document.ResolveReferences(false); - foreach (var item in errors) + // Resolve all references in all the documents loaded into the OpenApiWorkspace + foreach (var doc in openApiWorkSpace.Documents) { - diagnostic.Errors.Add(item); + errors.AddRange(doc.ResolveReferences(true)); } break; + case ReferenceResolutionSetting.ResolveLocalReferences: + errors.AddRange(document.ResolveReferences(false)); + break; case ReferenceResolutionSetting.DoNotResolveReferences: break; } + + foreach (var item in errors) + { + diagnostic.Errors.Add(item); + } } diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs index 275fbf7c0..9a5ba9213 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiRemoteReferenceCollector.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using System.Collections.Generic; +using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; @@ -19,9 +20,6 @@ public OpenApiRemoteReferenceCollector(OpenApiDocument document) _document = document; } - // TODO PathItem - // TODO Example - /// /// List of external references collected from OpenApiDocument /// @@ -33,60 +31,12 @@ public IEnumerable References } /// - /// Collect external reference - /// - public override void Visit(OpenApiParameter parameter) - { - AddReference(parameter.Reference); - } - - /// - /// Collect external reference - /// - public override void Visit(OpenApiCallback callback) - { - AddReference(callback.Reference); - - } - - /// - /// Collect external reference - /// - public override void Visit(OpenApiLink link) - { - AddReference(link.Reference); - } - - /// - /// Collect external reference - /// - public override void Visit(OpenApiRequestBody requestBody) - { - AddReference(requestBody.Reference); - } - - /// - /// Collect external reference - /// - public override void Visit(OpenApiResponse header) - { - AddReference(header.Reference); - } - - /// - /// Collect external reference - /// - public override void Visit(OpenApiHeader header) - { - AddReference(header.Reference); - } - - /// - /// Collect external reference + /// Collect reference for each reference /// - public override void Visit(OpenApiSchema schema) + /// + public override void Visit(IOpenApiReferenceable referenceable) { - AddReference(schema.Reference); + AddReference(referenceable.Reference); } /// @@ -104,7 +54,6 @@ private void AddReference(OpenApiReference reference) } } } - } - + } } } diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs index b40f815f4..8b84d2feb 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -43,7 +43,7 @@ internal async Task LoadAsync(OpenApiReference reference, OpenApiDocument docume // If not already in workspace, load it and process references if (!_workspace.Contains(item.ExternalResource)) { - var input = _loader.Load(new Uri(item.ExternalResource)); + var input = await _loader.LoadAsync(new Uri(item.ExternalResource, UriKind.RelativeOrAbsolute)); var result = await reader.ReadAsync(input); // TODO merge _diagnositics await LoadAsync(item, result.OpenApiDocument); } diff --git a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs index 99dfcd225..5baa580af 100644 --- a/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs +++ b/src/Microsoft.OpenApi.Readers/V2/OpenApiV2VersionService.cs @@ -161,6 +161,7 @@ public OpenApiReference ConvertToOpenApiReference(string reference, ReferenceTyp return new OpenApiReference { ExternalResource = segments[0], + Type = type, Id = segments[1].Substring(1) }; } diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs index 1630e60e1..2663b6a3b 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs @@ -69,6 +69,7 @@ public OpenApiReference ConvertToOpenApiReference( // "$ref": "Pet.json" return new OpenApiReference { + Type = type, ExternalResource = segments[0] }; } @@ -89,12 +90,18 @@ public OpenApiReference ConvertToOpenApiReference( // "$ref": "#/components/schemas/Pet" return ParseLocalReference(segments[1]); } - + // Where fragments point into a non-OpenAPI document, the id will be the complete fragment identifier + string id = segments[1]; // $ref: externalSource.yaml#/Pet + if (id.StartsWith("/components/")) + { + id = segments[1].Split('/')[3]; + } return new OpenApiReference { ExternalResource = segments[0], - Id = segments[1].Substring(1) + Type = type, + Id = id }; } } diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index b8aeb018e..0db9b2391 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -348,11 +348,15 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference, bool u return null; } - if (reference.IsExternal && !useExternal) + // Todo: Verify if we need to check to see if this external reference is actually targeted at this document. + if (useExternal) { - // Should not attempt to resolve external references against a single document. - throw new ArgumentException(Properties.SRResource.RemoteReferenceNotSupported); - } + if (this.Workspace == null) + { + throw new ArgumentException(Properties.SRResource.WorkspaceRequredForExternalReferenceResolution); + } + return this.Workspace.ResolveReference(reference); + } if (!reference.Type.HasValue) { diff --git a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs index 47f9493b9..96c0ce501 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs +++ b/src/Microsoft.OpenApi/Properties/SRResource.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.OpenApi.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class SRResource { @@ -357,5 +357,14 @@ internal static string Validation_StringMustBeEmailAddress { return ResourceManager.GetString("Validation_StringMustBeEmailAddress", resourceCulture); } } + + /// + /// Looks up a localized string similar to OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references.. + /// + internal static string WorkspaceRequredForExternalReferenceResolution { + get { + return ResourceManager.GetString("WorkspaceRequredForExternalReferenceResolution", resourceCulture); + } + } } } diff --git a/src/Microsoft.OpenApi/Properties/SRResource.resx b/src/Microsoft.OpenApi/Properties/SRResource.resx index 48f910a95..b2f129be2 100644 --- a/src/Microsoft.OpenApi/Properties/SRResource.resx +++ b/src/Microsoft.OpenApi/Properties/SRResource.resx @@ -216,4 +216,7 @@ The string '{0}' MUST be in the format of an email address. + + OpenAPI document must be added to an OpenApiWorkspace to be able to resolve external references. + \ No newline at end of file diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs index 0b69a7168..4e6a619a6 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs @@ -117,7 +117,7 @@ public IOpenApiReferenceable ResolveReference(OpenApiReference reference) { if (_documents.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var doc)) { - return doc.ResolveReference(reference, true); + return doc.ResolveReference(reference, false); } else if (_fragments.TryGetValue(new Uri(BaseUrl, reference.ExternalResource), out var fragment)) { diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index a4b588599..437baa662 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -14,6 +14,8 @@ + + @@ -232,6 +234,8 @@ Never + + Never diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index e37a8573d..f3b8d4fc1 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.OpenApi.Readers.Tests.OpenApiWorkspaceTests { public class OpenApiWorkspaceStreamTests { - + private const string SampleFolderPath = "V3Tests/Samples/OpenApiWorkspace/"; // Use OpenApiWorkspace to load a document and a referenced document @@ -45,6 +45,44 @@ public async Task LoadDocumentIntoWorkspace() Assert.NotNull(result.OpenApiDocument.Workspace); } + + + [Fact] + public async Task LoadTodoDocumentIntoWorkspace() + { + // Create a reader that will resolve all references + var reader = new OpenApiStreamReader(new OpenApiReaderSettings() + { + ReferenceResolution = ReferenceResolutionSetting.ResolveAllReferences, + CustomExternalLoader = new ResourceLoader() + }); + + ReadResult result; + using (var stream = Resources.GetStream("V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml")) + { + result = await reader.ReadAsync(stream); + } + + Assert.NotNull(result.OpenApiDocument.Workspace); + Assert.True(result.OpenApiDocument.Workspace.Contains("TodoComponents.yaml")); + var referencedSchema = result.OpenApiDocument + .Paths["/todos"] + .Operations[OperationType.Get] + .Responses["200"] + .Content["application/json"] + .Schema; + Assert.Equal("object", referencedSchema.Type); + Assert.Equal("string", referencedSchema.Properties["subject"].Type); + Assert.False(referencedSchema.UnresolvedReference); + + var referencedParameter = result.OpenApiDocument + .Paths["/todos"] + .Operations[OperationType.Get] + .Parameters + .Where(p => p.Name == "filter").FirstOrDefault(); + Assert.Equal("string", referencedParameter.Schema.Type); + + } } public class MockLoader : IStreamLoader @@ -59,4 +97,20 @@ public async Task LoadAsync(Uri uri) return null; } } + + + public class ResourceLoader : IStreamLoader + { + public Stream Load(Uri uri) + { + return null; + } + + public async Task LoadAsync(Uri uri) + { + var path = new Uri(new Uri("http://example.org/V3Tests/Samples/OpenApiWorkspace/"), uri).AbsolutePath; + path = path.Substring(1); // remove leading slash + return Resources.GetStream(path); + } + } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs index 6acf70500..04debfd7d 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/ReferenceService/ConvertToOpenApiReferenceV3Tests.cs @@ -17,8 +17,8 @@ public void ParseExternalReference() // Arrange var versionService = new OpenApiV3VersionService(); var externalResource = "externalSchema.json"; - var id = "externalPathSegment1/externalPathSegment2/externalPathSegment3"; - var input = $"{externalResource}#/{id}"; + var id = "/externalPathSegment1/externalPathSegment2/externalPathSegment3"; + var input = $"{externalResource}#{id}"; // Act var reference = versionService.ConvertToOpenApiReference(input, null); diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoComponents.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoComponents.yaml new file mode 100644 index 000000000..8602c4f5a --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoComponents.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.0 +info: + title: Components for the todo app + version: 1.0.0 +paths: {} +components: + parameters: + filter: + name: filter + in: query + schema: + type: string + schemas: + todo: + type: object + allof: + $ref: "#/components/schemas/entity" + properties: + subject: + type: string + entity: + type: object + properties: + id: + type:string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml new file mode 100644 index 000000000..7ac3d9ab8 --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiWorkspace/TodoMain.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + title: Example using a remote reference + version: 1.0.0 +paths: + "/todos": + get: + parameters: + - $ref: ./TodoComponents.yaml#/components/parameters/filter + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: ./TodoComponents.yaml#/components/schemas/todo \ No newline at end of file diff --git a/test/Microsoft.OpenApi.SmokeTests/WorkspaceTests.cs b/test/Microsoft.OpenApi.SmokeTests/WorkspaceTests.cs new file mode 100644 index 000000000..84f9d74ad --- /dev/null +++ b/test/Microsoft.OpenApi.SmokeTests/WorkspaceTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.OpenApi.SmokeTests +{ + public class WorkspaceTests + { + } +} From 36515c45ab483c832d79a1cb0077e500f106af41 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Tue, 2 Mar 2021 23:27:57 -0500 Subject: [PATCH 31/32] Added comments --- src/Microsoft.OpenApi.Readers/ReadResult.cs | 13 +++++++++++-- .../OpenApiWorkspaceStreamTests.cs | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/ReadResult.cs b/src/Microsoft.OpenApi.Readers/ReadResult.cs index 616c2f47c..7479d345f 100644 --- a/src/Microsoft.OpenApi.Readers/ReadResult.cs +++ b/src/Microsoft.OpenApi.Readers/ReadResult.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -8,11 +11,17 @@ namespace Microsoft.OpenApi.Readers { /// - /// + /// Container object used for returning the result of reading an OpenAPI description. /// public class ReadResult { + /// + /// The parsed OpenApiDocument. Null will be returned if the document could not be parsed. + /// public OpenApiDocument OpenApiDocument { set; get; } + /// + /// OpenApiDiagnostic contains the Errors reported while parsing + /// public OpenApiDiagnostic OpenApiDiagnostic { set; get; } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index f3b8d4fc1..f90a4e155 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -83,6 +83,8 @@ public async Task LoadTodoDocumentIntoWorkspace() Assert.Equal("string", referencedParameter.Schema.Type); } + + } public class MockLoader : IStreamLoader From 672508eac5e2ab46c3ce17248ce662e17fa73242 Mon Sep 17 00:00:00 2001 From: Darrel Miller Date: Sat, 13 Mar 2021 23:03:46 -0500 Subject: [PATCH 32/32] Fixed warnings and updated to just .net standard --- .../Microsoft.OpenApi.Readers.csproj | 10 +- .../Services/DefaultStreamLoader.cs | 5 +- .../Services/OpenApiWorkspaceLoader.cs | 3 +- src/Microsoft.OpenApi.Workbench/App.config | 2 +- .../Microsoft.OpenApi.Workbench.csproj | 2 +- .../Properties/Resources.Designer.cs | 7 +- .../Properties/Settings.Designer.cs | 2 +- .../OpenApiSerializableExtensions.cs | 1 - .../Microsoft.OpenApi.csproj | 4 +- .../Services/OpenApiReferenceError.cs | 8 + .../Writers/OpenApiYamlWriter.cs | 3 + .../Microsoft.OpenApi.Readers.Tests.csproj | 10 +- .../OpenApiWorkspaceStreamTests.cs | 6 +- .../Microsoft.OpenApi.SmokeTests.csproj | 8 +- .../Microsoft.OpenApi.Tests.csproj | 13 +- .../PublicApi/PublicApi.approved.txt | 272 +++++++++--------- .../PublicApi/PublicApiTests.cs | 2 +- 17 files changed, 182 insertions(+), 176 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj index 44e3f60ba..0f4996dde 100644 --- a/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj +++ b/src/Microsoft.OpenApi.Readers/Microsoft.OpenApi.Readers.csproj @@ -1,10 +1,10 @@  - net46; netstandard2.0 + netstandard2.0 true http://go.microsoft.com/fwlink/?LinkID=288890 https://github.com/Microsoft/OpenAPI.NET - https://raw.githubusercontent.com/Microsoft/OpenAPI.NET/master/LICENSE + MIT true Microsoft Microsoft @@ -26,17 +26,13 @@ - + - - - - True diff --git a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs index 865b43563..4659db711 100644 --- a/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/DefaultStreamLoader.cs @@ -2,11 +2,8 @@ // Licensed under the MIT license. using System; -using System.Collections.Generic; -using System.Net.Http; using System.IO; -using System.Linq; -using System.Text; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.OpenApi.Readers.Interface; diff --git a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs index 8b84d2feb..def92967e 100644 --- a/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs +++ b/src/Microsoft.OpenApi.Readers/Services/OpenApiWorkspaceLoader.cs @@ -15,8 +15,7 @@ internal class OpenApiWorkspaceLoader { private OpenApiWorkspace _workspace; private IStreamLoader _loader; - private OpenApiDiagnostic _diagnostics; - private OpenApiReaderSettings _readerSettings; + private readonly OpenApiReaderSettings _readerSettings; public OpenApiWorkspaceLoader(OpenApiWorkspace workspace, IStreamLoader loader, OpenApiReaderSettings readerSettings) { diff --git a/src/Microsoft.OpenApi.Workbench/App.config b/src/Microsoft.OpenApi.Workbench/App.config index bae5d6d81..4bfa00561 100644 --- a/src/Microsoft.OpenApi.Workbench/App.config +++ b/src/Microsoft.OpenApi.Workbench/App.config @@ -1,6 +1,6 @@ - + diff --git a/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj b/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj index 2614a478a..47a9d6ffe 100644 --- a/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj +++ b/src/Microsoft.OpenApi.Workbench/Microsoft.OpenApi.Workbench.csproj @@ -8,7 +8,7 @@ WinExe Microsoft.OpenApi.Workbench Microsoft.OpenApi.Workbench - v4.6.1 + v4.8 512 {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 4 diff --git a/src/Microsoft.OpenApi.Workbench/Properties/Resources.Designer.cs b/src/Microsoft.OpenApi.Workbench/Properties/Resources.Designer.cs index 8fb80b6b5..5aba72855 100644 --- a/src/Microsoft.OpenApi.Workbench/Properties/Resources.Designer.cs +++ b/src/Microsoft.OpenApi.Workbench/Properties/Resources.Designer.cs @@ -9,6 +9,9 @@ //------------------------------------------------------------------------------ namespace Microsoft.OpenApi.Workbench.Properties { + using System; + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -16,7 +19,7 @@ namespace Microsoft.OpenApi.Workbench.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -36,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenApiWorkbench.Properties.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.OpenApi.Workbench.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/Microsoft.OpenApi.Workbench/Properties/Settings.Designer.cs b/src/Microsoft.OpenApi.Workbench/Properties/Settings.Designer.cs index f17326ade..d15425180 100644 --- a/src/Microsoft.OpenApi.Workbench/Properties/Settings.Designer.cs +++ b/src/Microsoft.OpenApi.Workbench/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Microsoft.OpenApi.Workbench.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.3.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs index 52832097d..4694692ad 100755 --- a/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs +++ b/src/Microsoft.OpenApi/Extensions/OpenApiSerializableExtensions.cs @@ -50,7 +50,6 @@ public static void SerializeAsYaml(this T element, Stream stream, OpenApiSpec /// The given stream. /// The Open API specification version. /// The output format (JSON or YAML). - /// Provide configuration settings for controlling writing output public static void Serialize( this T element, Stream stream, diff --git a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj index c894aeb5a..228a1735b 100644 --- a/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj +++ b/src/Microsoft.OpenApi/Microsoft.OpenApi.csproj @@ -1,10 +1,10 @@  - net46; netstandard2.0 + netstandard2.0 true http://go.microsoft.com/fwlink/?LinkID=288890 https://github.com/Microsoft/OpenAPI.NET - https://raw.githubusercontent.com/Microsoft/OpenAPI.NET/master/LICENSE + MIT true Microsoft Microsoft diff --git a/src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs b/src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs index a35cfcb31..7e2ebdcac 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiReferenceError.cs @@ -11,6 +11,9 @@ namespace Microsoft.OpenApi.Services { + /// + /// + /// public class OpenApiReferenceError : OpenApiError { private OpenApiReference _reference; @@ -21,6 +24,11 @@ public OpenApiReferenceError(OpenApiException exception) : base(exception.Pointe { } + /// + /// + /// + /// + /// public OpenApiReferenceError(OpenApiReference reference, string message) : base("", message) { _reference = reference; diff --git a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs index 5cd93e184..47afdcc31 100644 --- a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs @@ -28,6 +28,9 @@ public OpenApiYamlWriter(TextWriter textWriter, OpenApiWriterSettings settings) } + /// + /// Allow rendering of multi-line strings using YAML | syntax + /// public bool UseLiteralStyle { get; set; } /// diff --git a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj index 437baa662..7f57e2313 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -1,6 +1,6 @@  - net46;net461 + net462 false Microsoft @@ -242,16 +242,16 @@ - - + + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs index f90a4e155..d684144cb 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/OpenApiWorkspaceTests/OpenApiWorkspaceStreamTests.cs @@ -94,7 +94,7 @@ public Stream Load(Uri uri) return null; } - public async Task LoadAsync(Uri uri) + public Task LoadAsync(Uri uri) { return null; } @@ -108,11 +108,11 @@ public Stream Load(Uri uri) return null; } - public async Task LoadAsync(Uri uri) + public Task LoadAsync(Uri uri) { var path = new Uri(new Uri("http://example.org/V3Tests/Samples/OpenApiWorkspace/"), uri).AbsolutePath; path = path.Substring(1); // remove leading slash - return Resources.GetStream(path); + return Task.FromResult(Resources.GetStream(path)); } } } diff --git a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj index 6b7413f4d..14c5ac6f2 100644 --- a/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj +++ b/test/Microsoft.OpenApi.SmokeTests/Microsoft.OpenApi.SmokeTests.csproj @@ -1,18 +1,18 @@  - net46;net461 + net462 - TRACE;DEBUG;net46 + TRACE;DEBUG;net462 - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj index b062bf15b..90d60ac6d 100755 --- a/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj +++ b/test/Microsoft.OpenApi.Tests/Microsoft.OpenApi.Tests.csproj @@ -1,6 +1,7 @@  - net46;net461 + net462 + false Microsoft Microsoft.OpenApi.Tests @@ -14,17 +15,17 @@ - - + + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt index 8468b5e84..1c74ffab7 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApi.approved.txt @@ -1,6 +1,6 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute(@"Microsoft.OpenApi.Readers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute(@"Microsoft.OpenApi.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] -[assembly: System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.6", FrameworkDisplayName=".NET Framework 4.6")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Microsoft.OpenApi.Readers.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"Microsoft.OpenApi.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100957cb48387b2a5f54f5ce39255f18f26d32a39990db27cf48737afc6bc62759ba996b8a2bfb675d4e39f3d06ecb55a178b1b4031dcb2a767e29977d88cce864a0d16bfc1b3bebb0edf9fe285f10fffc0a85f93d664fa05af07faa3aad2e545182dbf787e3fd32b56aca95df1a3c4e75dec164a3f1a4c653d971b01ffc39eb3c4")] +[assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName="")] namespace Microsoft.OpenApi.Any { public enum AnyType @@ -198,17 +198,17 @@ namespace Microsoft.OpenApi.Expressions public const string Prefix = "$"; protected RuntimeExpression() { } public abstract string Expression { get; } - public static Microsoft.OpenApi.Expressions.RuntimeExpression Build(string expression) { } - public override bool Equals(object obj) { } public bool Equals(Microsoft.OpenApi.Expressions.RuntimeExpression obj) { } + public override bool Equals(object obj) { } public override int GetHashCode() { } public override string ToString() { } + public static Microsoft.OpenApi.Expressions.RuntimeExpression Build(string expression) { } } public abstract class SourceExpression : Microsoft.OpenApi.Expressions.RuntimeExpression { protected SourceExpression(string value) { } protected string Value { get; } - public static Microsoft.OpenApi.Expressions.SourceExpression Build(string expression) { } + public new static Microsoft.OpenApi.Expressions.SourceExpression Build(string expression) { } } public sealed class StatusCodeExpression : Microsoft.OpenApi.Expressions.RuntimeExpression { @@ -225,45 +225,45 @@ namespace Microsoft.OpenApi.Expressions } namespace Microsoft.OpenApi.Extensions { - public class static EnumExtensions + public static class EnumExtensions { public static T GetAttributeOfType(this System.Enum enumValue) where T : System.Attribute { } public static string GetDisplayName(this System.Enum enumValue) { } } - public class static OpenApiElementExtensions + public static class OpenApiElementExtensions { public static System.Collections.Generic.IEnumerable Validate(this Microsoft.OpenApi.Interfaces.IOpenApiElement element, Microsoft.OpenApi.Validations.ValidationRuleSet ruleSet) { } } - public class static OpenApiExtensibleExtensions + public static class OpenApiExtensibleExtensions { public static void AddExtension(this T element, string name, Microsoft.OpenApi.Interfaces.IOpenApiExtension any) where T : Microsoft.OpenApi.Interfaces.IOpenApiExtensible { } } - public class static OpenApiReferencableExtensions + public static class OpenApiReferencableExtensions { public static Microsoft.OpenApi.Interfaces.IOpenApiReferenceable ResolveReference(this Microsoft.OpenApi.Interfaces.IOpenApiReferenceable element, Microsoft.OpenApi.JsonPointer pointer) { } } - public class static OpenApiSerializableExtensions + public static class OpenApiSerializableExtensions { - public static void Serialize(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion, Microsoft.OpenApi.OpenApiFormat format) - where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } - public static void Serialize(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion, Microsoft.OpenApi.OpenApiFormat format, Microsoft.OpenApi.Writers.OpenApiWriterSettings settings) + public static string Serialize(this T element, Microsoft.OpenApi.OpenApiSpecVersion specVersion, Microsoft.OpenApi.OpenApiFormat format) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } public static void Serialize(this T element, Microsoft.OpenApi.Writers.IOpenApiWriter writer, Microsoft.OpenApi.OpenApiSpecVersion specVersion) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } - public static string Serialize(this T element, Microsoft.OpenApi.OpenApiSpecVersion specVersion, Microsoft.OpenApi.OpenApiFormat format) + public static void Serialize(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion, Microsoft.OpenApi.OpenApiFormat format) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } - public static void SerializeAsJson(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion) + public static void Serialize(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion, Microsoft.OpenApi.OpenApiFormat format, Microsoft.OpenApi.Writers.OpenApiWriterSettings settings) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } public static string SerializeAsJson(this T element, Microsoft.OpenApi.OpenApiSpecVersion specVersion) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } - public static void SerializeAsYaml(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion) + public static void SerializeAsJson(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } public static string SerializeAsYaml(this T element, Microsoft.OpenApi.OpenApiSpecVersion specVersion) where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } + public static void SerializeAsYaml(this T element, System.IO.Stream stream, Microsoft.OpenApi.OpenApiSpecVersion specVersion) + where T : Microsoft.OpenApi.Interfaces.IOpenApiSerializable { } } - public class static StringExtensions + public static class StringExtensions { public static T GetEnumFromDisplayName(this string displayName) { } } @@ -343,7 +343,7 @@ namespace Microsoft.OpenApi.Models public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV3(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } } - public class static OpenApiConstants + public static class OpenApiConstants { public const string AccessCode = "accessCode"; public const string AdditionalProperties = "additionalProperties"; @@ -501,19 +501,19 @@ namespace Microsoft.OpenApi.Models public Microsoft.OpenApi.Services.OpenApiWorkspace Workspace { get; set; } public Microsoft.OpenApi.Interfaces.IOpenApiReferenceable ResolveReference(Microsoft.OpenApi.Models.OpenApiReference reference) { } public Microsoft.OpenApi.Interfaces.IOpenApiReferenceable ResolveReference(Microsoft.OpenApi.Models.OpenApiReference reference, bool useExternal) { } - public System.Collections.Generic.IEnumerable ResolveReferences(bool useExternal = False) { } + public System.Collections.Generic.IEnumerable ResolveReferences(bool useExternal = false) { } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV3(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } } public class OpenApiEncoding : Microsoft.OpenApi.Interfaces.IOpenApiElement, Microsoft.OpenApi.Interfaces.IOpenApiExtensible, Microsoft.OpenApi.Interfaces.IOpenApiSerializable { public OpenApiEncoding() { } - public System.Nullable AllowReserved { get; set; } + public bool? AllowReserved { get; set; } public string ContentType { get; set; } - public System.Nullable Explode { get; set; } + public bool? Explode { get; set; } public System.Collections.Generic.IDictionary Extensions { get; set; } public System.Collections.Generic.IDictionary Headers { get; set; } - public System.Nullable Style { get; set; } + public Microsoft.OpenApi.Models.ParameterStyle? Style { get; set; } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV3(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } } @@ -572,7 +572,7 @@ namespace Microsoft.OpenApi.Models public Microsoft.OpenApi.Models.OpenApiReference Reference { get; set; } public bool Required { get; set; } public Microsoft.OpenApi.Models.OpenApiSchema Schema { get; set; } - public System.Nullable Style { get; set; } + public Microsoft.OpenApi.Models.ParameterStyle? Style { get; set; } public bool UnresolvedReference { get; set; } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV2WithoutReference(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -683,12 +683,12 @@ namespace Microsoft.OpenApi.Models public System.Collections.Generic.IDictionary Examples { get; set; } public bool Explode { get; set; } public System.Collections.Generic.IDictionary Extensions { get; set; } - public System.Nullable In { get; set; } + public Microsoft.OpenApi.Models.ParameterLocation? In { get; set; } public string Name { get; set; } public Microsoft.OpenApi.Models.OpenApiReference Reference { get; set; } public bool Required { get; set; } public Microsoft.OpenApi.Models.OpenApiSchema Schema { get; set; } - public System.Nullable Style { get; set; } + public Microsoft.OpenApi.Models.ParameterStyle? Style { get; set; } public bool UnresolvedReference { get; set; } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV2WithoutReference(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } @@ -721,7 +721,7 @@ namespace Microsoft.OpenApi.Models public bool IsLocal { get; } public string ReferenceV2 { get; } public string ReferenceV3 { get; } - public System.Nullable Type { get; set; } + public Microsoft.OpenApi.Models.ReferenceType? Type { get; set; } public void SerializeAsV2(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } public void SerializeAsV3(Microsoft.OpenApi.Writers.IOpenApiWriter writer) { } } @@ -771,21 +771,21 @@ namespace Microsoft.OpenApi.Models public Microsoft.OpenApi.Models.OpenApiDiscriminator Discriminator { get; set; } public System.Collections.Generic.IList Enum { get; set; } public Microsoft.OpenApi.Any.IOpenApiAny Example { get; set; } - public System.Nullable ExclusiveMaximum { get; set; } - public System.Nullable ExclusiveMinimum { get; set; } + public bool? ExclusiveMaximum { get; set; } + public bool? ExclusiveMinimum { get; set; } public System.Collections.Generic.IDictionary Extensions { get; set; } public Microsoft.OpenApi.Models.OpenApiExternalDocs ExternalDocs { get; set; } public string Format { get; set; } public Microsoft.OpenApi.Models.OpenApiSchema Items { get; set; } - public System.Nullable MaxItems { get; set; } - public System.Nullable MaxLength { get; set; } - public System.Nullable MaxProperties { get; set; } - public System.Nullable Maximum { get; set; } - public System.Nullable MinItems { get; set; } - public System.Nullable MinLength { get; set; } - public System.Nullable MinProperties { get; set; } - public System.Nullable Minimum { get; set; } - public System.Nullable MultipleOf { get; set; } + public int? MaxItems { get; set; } + public int? MaxLength { get; set; } + public int? MaxProperties { get; set; } + public decimal? Maximum { get; set; } + public int? MinItems { get; set; } + public int? MinLength { get; set; } + public int? MinProperties { get; set; } + public decimal? Minimum { get; set; } + public decimal? MultipleOf { get; set; } public Microsoft.OpenApi.Models.OpenApiSchema Not { get; set; } public bool Nullable { get; set; } public System.Collections.Generic.IList OneOf { get; set; } @@ -796,7 +796,7 @@ namespace Microsoft.OpenApi.Models public System.Collections.Generic.ISet Required { get; set; } public string Title { get; set; } public string Type { get; set; } - public System.Nullable UniqueItems { get; set; } + public bool? UniqueItems { get; set; } public bool UnresolvedReference { get; set; } public bool WriteOnly { get; set; } public Microsoft.OpenApi.Models.OpenApiXml Xml { get; set; } @@ -953,7 +953,7 @@ namespace Microsoft.OpenApi.Services public string Extension { get; } public string Header { get; } public string Link { get; set; } - public System.Nullable Operation { get; set; } + public Microsoft.OpenApi.Models.OperationType? Operation { get; set; } public string Path { get; set; } public string Response { get; set; } public string ServerVariable { get; } @@ -963,7 +963,7 @@ namespace Microsoft.OpenApi.Services public OpenApiAnyComparer() { } public override void Compare(Microsoft.OpenApi.Any.IOpenApiAny source, Microsoft.OpenApi.Any.IOpenApiAny target, Microsoft.OpenApi.Services.ComparisonContext comparisonContext) { } } - public class static OpenApiComparer + public static class OpenApiComparer { public static System.Collections.Generic.IEnumerable Compare(Microsoft.OpenApi.Models.OpenApiDocument source, Microsoft.OpenApi.Models.OpenApiDocument target) { } } @@ -1158,48 +1158,48 @@ namespace Microsoft.OpenApi.Services public string PathString { get; } public void Enter(string segment) { } public void Exit() { } + public virtual void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtensible openApiExtensible) { } + public virtual void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtension openApiExtension) { } + public virtual void Visit(Microsoft.OpenApi.Interfaces.IOpenApiReferenceable referenceable) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiCallback callback) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiComponents components) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiContact contact) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiDocument doc) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiEncoding encoding) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiExample example) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiExternalDocs externalDocs) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiHeader tag) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiInfo info) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiContact contact) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiLicense license) { } - public virtual void Visit(System.Collections.Generic.IList servers) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiServer server) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiPaths paths) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiPathItem pathItem) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiServerVariable serverVariable) { } - public virtual void Visit(System.Collections.Generic.IDictionary operations) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiLink link) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiMediaType mediaType) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiOAuthFlow openApiOAuthFlow) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiOperation operation) { } - public virtual void Visit(System.Collections.Generic.IList parameters) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiParameter parameter) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiPathItem pathItem) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiPaths paths) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiRequestBody requestBody) { } - public virtual void Visit(System.Collections.Generic.IDictionary headers) { } - public virtual void Visit(System.Collections.Generic.IDictionary callbacks) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiResponse response) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiResponses response) { } - public virtual void Visit(System.Collections.Generic.IDictionary content) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiMediaType mediaType) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiEncoding encoding) { } - public virtual void Visit(System.Collections.Generic.IDictionary examples) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiComponents components) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiExternalDocs externalDocs) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiSchema schema) { } - public virtual void Visit(System.Collections.Generic.IDictionary links) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiLink link) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiCallback callback) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiTag tag) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiHeader tag) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiOAuthFlow openApiOAuthFlow) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiSecurityRequirement securityRequirement) { } public virtual void Visit(Microsoft.OpenApi.Models.OpenApiSecurityScheme securityScheme) { } - public virtual void Visit(Microsoft.OpenApi.Models.OpenApiExample example) { } - public virtual void Visit(System.Collections.Generic.IList openApiTags) { } - public virtual void Visit(System.Collections.Generic.IList openApiSecurityRequirements) { } - public virtual void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtensible openApiExtensible) { } - public virtual void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtension openApiExtension) { } - public virtual void Visit(System.Collections.Generic.IList example) { } - public virtual void Visit(System.Collections.Generic.IDictionary serverVariables) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiServer server) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiServerVariable serverVariable) { } + public virtual void Visit(Microsoft.OpenApi.Models.OpenApiTag tag) { } + public virtual void Visit(System.Collections.Generic.IDictionary operations) { } + public virtual void Visit(System.Collections.Generic.IDictionary callbacks) { } public virtual void Visit(System.Collections.Generic.IDictionary encodings) { } - public virtual void Visit(Microsoft.OpenApi.Interfaces.IOpenApiReferenceable referenceable) { } + public virtual void Visit(System.Collections.Generic.IDictionary examples) { } + public virtual void Visit(System.Collections.Generic.IDictionary headers) { } + public virtual void Visit(System.Collections.Generic.IDictionary links) { } + public virtual void Visit(System.Collections.Generic.IDictionary content) { } + public virtual void Visit(System.Collections.Generic.IDictionary serverVariables) { } + public virtual void Visit(System.Collections.Generic.IList example) { } + public virtual void Visit(System.Collections.Generic.IList parameters) { } + public virtual void Visit(System.Collections.Generic.IList openApiSecurityRequirements) { } + public virtual void Visit(System.Collections.Generic.IList servers) { } + public virtual void Visit(System.Collections.Generic.IList openApiTags) { } } public class OpenApiWalker { @@ -1208,8 +1208,8 @@ namespace Microsoft.OpenApi.Services } public class OpenApiWorkspace { - public OpenApiWorkspace(System.Uri baseUrl) { } public OpenApiWorkspace() { } + public OpenApiWorkspace(System.Uri baseUrl) { } public System.Collections.Generic.IEnumerable Artifacts { get; } public System.Uri BaseUrl { get; } public System.Collections.Generic.IEnumerable Documents { get; } @@ -1236,25 +1236,25 @@ namespace Microsoft.OpenApi.Validations public OpenApiValidator(Microsoft.OpenApi.Validations.ValidationRuleSet ruleSet) { } public System.Collections.Generic.IEnumerable Errors { get; } public void AddError(Microsoft.OpenApi.Validations.OpenApiValidatorError error) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiDocument item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiInfo item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiContact item) { } + public override void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtensible item) { } + public override void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtension item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiCallback item) { } public override void Visit(Microsoft.OpenApi.Models.OpenApiComponents item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiHeader item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiResponse item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiMediaType item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiResponses item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiContact item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiDocument item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiEncoding item) { } public override void Visit(Microsoft.OpenApi.Models.OpenApiExternalDocs item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiHeader item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiInfo item) { } public override void Visit(Microsoft.OpenApi.Models.OpenApiLicense item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiMediaType item) { } public override void Visit(Microsoft.OpenApi.Models.OpenApiOAuthFlow item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiTag item) { } public override void Visit(Microsoft.OpenApi.Models.OpenApiParameter item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiResponse item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiResponses item) { } public override void Visit(Microsoft.OpenApi.Models.OpenApiSchema item) { } public override void Visit(Microsoft.OpenApi.Models.OpenApiServer item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiEncoding item) { } - public override void Visit(Microsoft.OpenApi.Models.OpenApiCallback item) { } - public override void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtensible item) { } - public override void Visit(Microsoft.OpenApi.Interfaces.IOpenApiExtension item) { } + public override void Visit(Microsoft.OpenApi.Models.OpenApiTag item) { } public override void Visit(System.Collections.Generic.IList items) { } } public class OpenApiValidatorError : Microsoft.OpenApi.Models.OpenApiError @@ -1262,7 +1262,7 @@ namespace Microsoft.OpenApi.Validations public OpenApiValidatorError(string ruleName, string pointer, string message) { } public string RuleName { get; set; } } - public class static ValidationContextExtensions + public static class ValidationContextExtensions { public static void CreateError(this Microsoft.OpenApi.Validations.IValidationContext context, string ruleName, string message) { } } @@ -1278,9 +1278,9 @@ namespace Microsoft.OpenApi.Validations public System.Collections.Generic.IList Rules { get; } public void Add(Microsoft.OpenApi.Validations.ValidationRule rule) { } public System.Collections.Generic.IList FindRules(System.Type type) { } + public System.Collections.Generic.IEnumerator GetEnumerator() { } public static Microsoft.OpenApi.Validations.ValidationRuleSet GetDefaultRuleSet() { } public static Microsoft.OpenApi.Validations.ValidationRuleSet GetEmptyRuleSet() { } - public System.Collections.Generic.IEnumerator GetEnumerator() { } } public class ValidationRule : Microsoft.OpenApi.Validations.ValidationRule where T : Microsoft.OpenApi.Interfaces.IOpenApiElement @@ -1290,98 +1290,98 @@ namespace Microsoft.OpenApi.Validations } namespace Microsoft.OpenApi.Validations.Rules { - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiComponentsRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiComponentsRules { public static System.Text.RegularExpressions.Regex KeyRegex; public static Microsoft.OpenApi.Validations.ValidationRule KeyMustBeRegularExpression { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiContactRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiContactRules { public static Microsoft.OpenApi.Validations.ValidationRule EmailMustBeEmailFormat { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiDocumentRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiDocumentRules { public static Microsoft.OpenApi.Validations.ValidationRule OpenApiDocumentFieldIsMissing { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiExtensibleRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiExtensibleRules { public static Microsoft.OpenApi.Validations.ValidationRule ExtensionNameMustStartWithXDash { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiExternalDocsRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiExternalDocsRules { public static Microsoft.OpenApi.Validations.ValidationRule UrlIsRequired { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiHeaderRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiHeaderRules { public static Microsoft.OpenApi.Validations.ValidationRule HeaderMismatchedDataType { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiInfoRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiInfoRules { public static Microsoft.OpenApi.Validations.ValidationRule InfoRequiredFields { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiLicenseRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiLicenseRules { public static Microsoft.OpenApi.Validations.ValidationRule LicenseRequiredFields { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiMediaTypeRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiMediaTypeRules { public static Microsoft.OpenApi.Validations.ValidationRule MediaTypeMismatchedDataType { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiOAuthFlowRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiOAuthFlowRules { public static Microsoft.OpenApi.Validations.ValidationRule OAuthFlowRequiredFields { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiParameterRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiParameterRules { public static Microsoft.OpenApi.Validations.ValidationRule ParameterMismatchedDataType { get; } public static Microsoft.OpenApi.Validations.ValidationRule ParameterRequiredFields { get; } public static Microsoft.OpenApi.Validations.ValidationRule RequiredMustBeTrueWhenInIsPath { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiPathsRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiPathsRules { public static Microsoft.OpenApi.Validations.ValidationRule PathNameMustBeginWithSlash { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiResponseRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiResponseRules { public static Microsoft.OpenApi.Validations.ValidationRule ResponseRequiredFields { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiResponsesRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiResponsesRules { public static Microsoft.OpenApi.Validations.ValidationRule ResponsesMustBeIdentifiedByDefaultOrStatusCode { get; } public static Microsoft.OpenApi.Validations.ValidationRule ResponsesMustContainAtLeastOneResponse { get; } } - [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.All, AllowMultiple=false, Inherited=false)] + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.All, AllowMultiple=false, Inherited=false)] public class OpenApiRuleAttribute : System.Attribute { public OpenApiRuleAttribute() { } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiSchemaRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiSchemaRules { public static Microsoft.OpenApi.Validations.ValidationRule SchemaMismatchedDataType { get; } public static Microsoft.OpenApi.Validations.ValidationRule ValidateSchemaDiscriminator { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiServerRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiServerRules { public static Microsoft.OpenApi.Validations.ValidationRule ServerRequiredFields { get; } } - [Microsoft.OpenApi.Validations.Rules.OpenApiRuleAttribute()] - public class static OpenApiTagRules + [Microsoft.OpenApi.Validations.Rules.OpenApiRule] + public static class OpenApiTagRules { public static Microsoft.OpenApi.Validations.ValidationRule TagRequiredFields { get; } } @@ -1403,11 +1403,11 @@ namespace Microsoft.OpenApi.Writers void WriteRaw(string value); void WriteStartArray(); void WriteStartObject(); - void WriteValue(string value); + void WriteValue(bool value); void WriteValue(decimal value); void WriteValue(int value); - void WriteValue(bool value); void WriteValue(object value); + void WriteValue(string value); } public class OpenApiJsonWriter : Microsoft.OpenApi.Writers.OpenApiWriterBase { @@ -1424,7 +1424,7 @@ namespace Microsoft.OpenApi.Writers public override void WriteValue(string value) { } protected override void WriteValueSeparator() { } } - public class static OpenApiWriterAnyExtensions + public static class OpenApiWriterAnyExtensions { public static void WriteAny(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, T any) where T : Microsoft.OpenApi.Any.IOpenApiAny { } @@ -1457,19 +1457,19 @@ namespace Microsoft.OpenApi.Writers public abstract void WriteRaw(string value); public abstract void WriteStartArray(); public abstract void WriteStartObject(); - public abstract void WriteValue(string value); - public virtual void WriteValue(float value) { } - public virtual void WriteValue(double value) { } + public virtual void WriteValue(bool value) { } + public virtual void WriteValue(System.DateTime value) { } + public virtual void WriteValue(System.DateTimeOffset value) { } public virtual void WriteValue(decimal value) { } + public virtual void WriteValue(double value) { } public virtual void WriteValue(int value) { } public virtual void WriteValue(long value) { } - public virtual void WriteValue(System.DateTime value) { } - public virtual void WriteValue(System.DateTimeOffset value) { } - public virtual void WriteValue(bool value) { } public virtual void WriteValue(object value) { } + public virtual void WriteValue(float value) { } + public abstract void WriteValue(string value); protected abstract void WriteValueSeparator(); } - public class static OpenApiWriterExtensions + public static class OpenApiWriterExtensions { public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable elements, System.Action action) { } public static void WriteOptionalCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable elements, System.Action action) @@ -1482,12 +1482,12 @@ namespace Microsoft.OpenApi.Writers public static void WriteOptionalObject(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, T value, System.Action action) where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { } public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, string value) { } - public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool value, bool defaultValue = False) { } - public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Nullable value, bool defaultValue = False) { } - public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Nullable value) - where T : struct { } + public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool value, bool defaultValue = false) { } + public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, bool? value, bool defaultValue = false) { } public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, T value) where T : struct { } + public static void WriteProperty(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, T? value) + where T : struct { } public static void WriteRequiredCollection(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IEnumerable elements, System.Action action) where T : Microsoft.OpenApi.Interfaces.IOpenApiElement { } public static void WriteRequiredMap(this Microsoft.OpenApi.Writers.IOpenApiWriter writer, string name, System.Collections.Generic.IDictionary elements, System.Action action) { } @@ -1536,5 +1536,5 @@ namespace Microsoft.OpenApi.Writers Object = 0, Array = 1, } - public class static SpecialCharacterStringExtensions { } + public static class SpecialCharacterStringExtensions { } } \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApiTests.cs b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApiTests.cs index a4eed558e..31671fc34 100755 --- a/test/Microsoft.OpenApi.Tests/PublicApi/PublicApiTests.cs +++ b/test/Microsoft.OpenApi.Tests/PublicApi/PublicApiTests.cs @@ -26,7 +26,7 @@ public void ReviewPublicApiChanges() // It takes a human to read the change, determine if it is breaking and update the PublicApi.approved.txt with the new approved API surface // Arrange - var publicApi = ApiGenerator.GeneratePublicApi(typeof(OpenApiSpecVersion).Assembly, whitelistedNamespacePrefixes: new[] { "Microsoft.OpenApi" }); + var publicApi = typeof(OpenApiSpecVersion).Assembly.GeneratePublicApi(new ApiGeneratorOptions() { WhitelistedNamespacePrefixes = new[] { "Microsoft.OpenApi" } } ); // Act var approvedFilePath = Path.Combine("PublicApi", "PublicApi.approved.txt");