From f6cc9d8b5d598ec6f3b70ad779053c9cd4a8f907 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 17 Oct 2022 13:20:44 +0300 Subject: [PATCH 01/21] Add webhooks property to OpenAPI document --- src/Microsoft.OpenApi/Models/OpenApiDocument.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 5177e4f45..e4bbd9a45 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -39,6 +39,13 @@ public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible /// public OpenApiPaths Paths { get; set; } + /// + /// The incoming webhooks that MAY be received as part of this API and that the API consumer MAY choose to implement. + /// A map of requests initiated other than by an API call, for example by an out of band registration. + /// The key name is a unique string to refer to each webhook, while the (optionally referenced) Path Item Object describes a request that may be initiated by the API provider and the expected responses + /// + public IDictionary Webhooks { get; set; } = new Dictionary(); + /// /// An element to hold various schemas for the specification. /// From 06783653f828fa878dcb2baf74efc79e1978ed8f Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 17 Oct 2022 13:21:11 +0300 Subject: [PATCH 02/21] Deep copy the webhooks object in the copy constructor --- src/Microsoft.OpenApi/Models/OpenApiDocument.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index e4bbd9a45..a82653c7e 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -91,6 +91,7 @@ public OpenApiDocument(OpenApiDocument document) Info = document?.Info != null ? new(document?.Info) : null; Servers = document?.Servers != null ? new List(document.Servers) : null; Paths = document?.Paths != null ? new(document?.Paths) : null; + Webhooks = document?.Webhooks != null ? new Dictionary(document.Webhooks) : null; Components = document?.Components != null ? new(document?.Components) : null; SecurityRequirements = document?.SecurityRequirements != null ? new List(document.SecurityRequirements) : null; Tags = document?.Tags != null ? new List(document.Tags) : null; From 5bb8f441b028bed6ffcdff880e073772544f9b68 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 17 Oct 2022 13:21:33 +0300 Subject: [PATCH 03/21] Add serialization for the webhooks property --- .../Models/OpenApiDocument.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index a82653c7e..f311e5c12 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -123,6 +123,24 @@ public void SerializeAsV3(IOpenApiWriter writer) // paths writer.WriteRequiredObject(OpenApiConstants.Paths, Paths, (w, p) => p.SerializeAsV3(w)); + // webhooks + writer.WriteOptionalMap( + OpenApiConstants.Webhooks, + Webhooks, + (w, key, component) => + { + if (component.Reference != null && + component.Reference.Type == ReferenceType.Schema && + component.Reference.Id == key) + { + component.SerializeAsV3WithoutReference(w); + } + else + { + component.SerializeAsV3(w); + } + }); + // components writer.WriteOptionalObject(OpenApiConstants.Components, Components, (w, c) => c.SerializeAsV3(w)); From 7479780ff9c305a71dae7057875af4aeadf683e9 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 17 Oct 2022 13:22:04 +0300 Subject: [PATCH 04/21] Add logic to deserialize the webhooks property --- .../V3/OpenApiDocumentDeserializer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs index df1434cd9..db5a7462a 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System.Collections.Generic; @@ -26,6 +26,7 @@ internal static partial class OpenApiV3Deserializer {"info", (o, n) => o.Info = LoadInfo(n)}, {"servers", (o, n) => o.Servers = n.CreateList(LoadServer)}, {"paths", (o, n) => o.Paths = LoadPaths(n)}, + {"webhooks", (o, n) => o.Webhooks = n.CreateMapWithReference(ReferenceType.PathItem, LoadPathItem)}, {"components", (o, n) => o.Components = LoadComponents(n)}, {"tags", (o, n) => {o.Tags = n.CreateList(LoadTag); foreach (var tag in o.Tags) From 43e165cc06e601faf9b0402c5d35d7d27d71b2f7 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 17 Oct 2022 13:22:29 +0300 Subject: [PATCH 05/21] Clean up project references --- .../V3/OpenApiDocumentDeserializer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs index db5a7462a..33a9f706a 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs @@ -1,10 +1,7 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -using System.Collections.Generic; -using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Extensions; -using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers.ParseNodes; From 9efc13020462b8550a9f0515e1463a323837ac70 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 17 Oct 2022 13:23:21 +0300 Subject: [PATCH 06/21] Add pathItem reference type and webhooks constant --- src/Microsoft.OpenApi/Models/OpenApiConstants.cs | 5 +++++ src/Microsoft.OpenApi/Models/OpenApiDocument.cs | 2 +- src/Microsoft.OpenApi/Models/ReferenceType.cs | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs index 553844764..40c082915 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiConstants.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiConstants.cs @@ -20,6 +20,11 @@ public static class OpenApiConstants /// public const string Info = "info"; + /// + /// Field: Webhooks + /// + public const string Webhooks = "webhooks"; + /// /// Field: Title /// diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index f311e5c12..5a73bc8a0 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; diff --git a/src/Microsoft.OpenApi/Models/ReferenceType.cs b/src/Microsoft.OpenApi/Models/ReferenceType.cs index 6ac0c9ed2..b86f3d171 100644 --- a/src/Microsoft.OpenApi/Models/ReferenceType.cs +++ b/src/Microsoft.OpenApi/Models/ReferenceType.cs @@ -58,6 +58,11 @@ public enum ReferenceType /// /// Tags item. /// - [Display("tags")] Tag + [Display("tags")] Tag, + + /// + /// Path item. + /// + [Display("pathItem")] PathItem, } } From c79a4f9bb7efefda4f446118e34730cc5f2e2565 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 24 Oct 2022 13:16:12 +0300 Subject: [PATCH 07/21] Adds tests --- .../Microsoft.OpenApi.Readers.Tests.csproj | 3 + .../V3Tests/OpenApiDocumentTests.cs | 213 ++++++++++++++++++ .../OpenApiDocument/documentWithWebhooks.yaml | 81 +++++++ ...orks_produceTerseOutput=False.verified.txt | 51 +++++ ...Works_produceTerseOutput=True.verified.txt | 1 + .../Models/OpenApiDocumentTests.cs | 139 +++++++++++- 6 files changed, 486 insertions(+), 2 deletions(-) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=False.verified.txt create mode 100644 test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=True.verified.txt 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 1579f85e5..35b0595d9 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj +++ b/test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj @@ -137,6 +137,9 @@ Never + + Never + Never diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index 6fbb7065a..b31e1a6ce 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -1327,5 +1327,218 @@ public void HeaderParameterShouldAllowExample() }); } } + + [Fact] + public void ParseDocumentWithWebhooksShouldSucceed() + { + // Arrange and Act + using var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "documentWithWebhooks.yaml")); + var actual = new OpenApiStreamReader().Read(stream, out var diagnostic); + + var components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["pet"] = new OpenApiSchema + { + Type = "object", + Required = new HashSet + { + "id", + "name" + }, + Properties = new Dictionary + { + ["id"] = new OpenApiSchema + { + Type = "integer", + Format = "int64" + }, + ["name"] = new OpenApiSchema + { + Type = "string" + }, + ["tag"] = new OpenApiSchema + { + Type = "string" + }, + }, + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "pet", + HostDocument = actual + } + }, + ["newPet"] = new OpenApiSchema + { + Type = "object", + Required = new HashSet + { + "name" + }, + Properties = new Dictionary + { + ["id"] = new OpenApiSchema + { + Type = "integer", + Format = "int64" + }, + ["name"] = new OpenApiSchema + { + Type = "string" + }, + ["tag"] = new OpenApiSchema + { + Type = "string" + }, + }, + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = "newPet", + HostDocument = actual + } + } + } + }; + + // Create a clone of the schema to avoid modifying things in components. + var petSchema = Clone(components.Schemas["pet"]); + + petSchema.Reference = new OpenApiReference + { + Id = "pet", + Type = ReferenceType.Schema, + HostDocument = actual + }; + + var newPetSchema = Clone(components.Schemas["newPet"]); + + newPetSchema.Reference = new OpenApiReference + { + Id = "newPet", + Type = ReferenceType.Schema, + HostDocument = actual + }; + + var expected = new OpenApiDocument + { + Info = new OpenApiInfo + { + Version = "1.0.0", + Title = "Webhook Example" + }, + Webhooks = new OpenApiPaths + { + ["/pets"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Get] = new OpenApiOperation + { + Description = "Returns all pets from the system that the user has access to", + OperationId = "findPets", + Parameters = new List + { + new OpenApiParameter + { + Name = "tags", + In = ParameterLocation.Query, + Description = "tags to filter by", + Required = false, + Schema = new OpenApiSchema + { + Type = "array", + Items = new OpenApiSchema + { + Type = "string" + } + } + }, + new OpenApiParameter + { + Name = "limit", + In = ParameterLocation.Query, + Description = "maximum number of results to return", + Required = false, + Schema = new OpenApiSchema + { + Type = "integer", + Format = "int32" + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "pet response", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = petSchema + } + }, + ["application/xml"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = "array", + Items = petSchema + } + } + } + } + } + }, + [OperationType.Post] = new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "Information about a new pet in the system", + Required = true, + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = newPetSchema + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "Return a 200 status to indicate that the data was received successfully", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = petSchema + }, + } + } + } + } + }, + Reference = new OpenApiReference + { + Type = ReferenceType.PathItem, + Id = "/pets" + } + } + }, + Components = components + }; + + // Assert + diagnostic.Should().BeEquivalentTo(new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 }); + actual.Should().BeEquivalentTo(expected); + } } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml new file mode 100644 index 000000000..11855036d --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml @@ -0,0 +1,81 @@ +openapi: 3.0.1 +info: + title: Webhook Example + version: 1.0.0 +webhooks: + /pets: + get: + description: Returns all pets from the system that the user has access to + operationId: findPets + parameters: + - name: tags + in: query + description: tags to filter by + required: false + schema: + type: array + items: + type: string + - name: limit + in: query + description: maximum number of results to return + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: pet response + content: + application/json: + schema: + type: array + items: + "$ref": '#/components/schemas/pet' + application/xml: + schema: + type: array + items: + "$ref": '#/components/schemas/pet' + post: + requestBody: + description: Information about a new pet in the system + required: true + content: + 'application/json': + schema: + "$ref": '#/components/schemas/newPet' + responses: + "200": + description: Return a 200 status to indicate that the data was received successfully + content: + application/json: + schema: + $ref: '#/components/schemas/pet' +components: + schemas: + pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + newPet: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=False.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=False.verified.txt new file mode 100644 index 000000000..73cc1b716 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=False.verified.txt @@ -0,0 +1,51 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Webhook Example", + "version": "1.0.0" + }, + "paths": { }, + "webhooks": { + "newPet": { + "post": { + "requestBody": { + "description": "Information about a new pet in the system", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "responses": { + "200": { + "description": "Return a 200 status to indicate that the data was received successfully" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=True.verified.txt b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=True.verified.txt new file mode 100644 index 000000000..a23dd5675 --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.SerializeDocumentWithWebhooksAsV3JsonWorks_produceTerseOutput=True.verified.txt @@ -0,0 +1 @@ +{"openapi":"3.0.1","info":{"title":"Webhook Example","version":"1.0.0"},"paths":{},"webhooks":{"newPet":{"post":{"requestBody":{"description":"Information about a new pet in the system","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pet"}}}},"responses":{"200":{"description":"Return a 200 status to indicate that the data was received successfully"}}}}},"components":{"schemas":{"Pet":{"required":["id","name"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"tag":{"type":"string"}}}}}} \ 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 89289397f..6a185b556 100644 --- a/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Tests/Models/OpenApiDocumentTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -890,6 +890,78 @@ public class OpenApiDocumentTests Components = AdvancedComponents }; + public static OpenApiDocument DocumentWithWebhooks = new OpenApiDocument() + { + Info = new OpenApiInfo + { + Title = "Webhook Example", + Version = "1.0.0" + }, + Webhooks = new Dictionary + { + ["newPet"] = new OpenApiPathItem + { + Operations = new Dictionary + { + [OperationType.Post] = new OpenApiOperation + { + RequestBody = new OpenApiRequestBody + { + Description = "Information about a new pet in the system", + Content = new Dictionary + { + ["application/json"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = "Pet", + Type = ReferenceType.Schema + } + } + } + } + }, + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse + { + Description = "Return a 200 status to indicate that the data was received successfully" + } + } + } + } + } + }, + Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["Pet"] = new OpenApiSchema + { + Required = new HashSet { "id", "name" }, + Properties = new Dictionary + { + ["id"] = new OpenApiSchema + { + Type = "integer", + Format = "int64" + }, + ["name"] = new OpenApiSchema + { + Type = "string" + }, + ["tag"] = new OpenApiSchema + { + Type = "string" + } + } + } + } + } + }; + public static OpenApiDocument DuplicateExtensions = new OpenApiDocument { Info = new OpenApiInfo @@ -1319,7 +1391,7 @@ public void SerializeRelativeRootPathWithHostAsV2JsonWorks() public void TestHashCodesForSimilarOpenApiDocuments() { // Arrange - var sampleFolderPath = "Models/Samples/"; + var sampleFolderPath = "Models/Samples/"; var doc1 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocument.yaml")); var doc2 = ParseInputFile(Path.Combine(sampleFolderPath, "sampleDocument.yaml")); @@ -1356,5 +1428,68 @@ public void CopyConstructorForAdvancedDocumentWorks() Assert.Equal(2, doc.Paths.Count); Assert.NotNull(doc.Components); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async void SerializeDocumentWithWebhooksAsV3JsonWorks(bool produceTerseOutput) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiJsonWriter(outputStringWriter, new OpenApiJsonWriterSettings { Terse = produceTerseOutput }); + + // Act + DocumentWithWebhooks.SerializeAsV3(writer); + writer.Flush(); + var actual = outputStringWriter.GetStringBuilder().ToString(); + + // Assert + await Verifier.Verify(actual).UseParameters(produceTerseOutput); + } + + [Fact] + public void SerializeDocumentWithWebhooksAsV3YamlWorks() + { + // Arrange + var expected = @"openapi: 3.0.1 +info: + title: Webhook Example + version: 1.0.0 +paths: { } +webhooks: + newPet: + post: + requestBody: + description: Information about a new pet in the system + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: Return a 200 status to indicate that the data was received successfully +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string"; + + // Act + var actual = DocumentWithWebhooks.SerializeAsYaml(OpenApiSpecVersion.OpenApi3_0); + + // Assert + actual = actual.MakeLineBreaksEnvironmentNeutral(); + expected = expected.MakeLineBreaksEnvironmentNeutral(); + Assert.Equal(expected, actual); + } } } From 103f123c544f229c3b547284f8f3538ad48c5b8f Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 24 Oct 2022 13:18:53 +0300 Subject: [PATCH 08/21] Adds 3.1 as a valid input OpenAPI version --- src/Microsoft.OpenApi.Readers/ParsingContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs index 6c4dece2f..659f053c6 100644 --- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs +++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs @@ -65,7 +65,7 @@ internal OpenApiDocument Parse(YamlDocument yamlDocument) this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0; break; - case string version when version.StartsWith("3.0"): + case string version when version.StartsWith("3.0") || version.StartsWith("3.1"): VersionService = new OpenApiV3VersionService(Diagnostic); doc = VersionService.LoadDocument(RootNode); this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_0; From 624bd0ce752b61ef7308052c2e8cf8a6976db732 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 24 Oct 2022 13:20:32 +0300 Subject: [PATCH 09/21] Adds a walker to visit the webhooks object and its child elements --- .../Services/OpenApiVisitorBase.cs | 7 ++++++ .../Services/OpenApiWalker.cs | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs index c9679381a..85a90a0ef 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs @@ -99,6 +99,13 @@ public virtual void Visit(OpenApiPaths paths) { } + /// + /// Visits Webhooks> + /// + public virtual void Visit(IDictionary webhooks) + { + } + /// /// Visits /// diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 78ca5e61b..42afba695 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -46,6 +46,7 @@ public void Walk(OpenApiDocument doc) Walk(OpenApiConstants.Info, () => Walk(doc.Info)); Walk(OpenApiConstants.Servers, () => Walk(doc.Servers)); Walk(OpenApiConstants.Paths, () => Walk(doc.Paths)); + Walk(OpenApiConstants.Webhooks, () => Walk(doc.Webhooks)); Walk(OpenApiConstants.Components, () => Walk(doc.Components)); Walk(OpenApiConstants.Security, () => Walk(doc.SecurityRequirements)); Walk(OpenApiConstants.ExternalDocs, () => Walk(doc.ExternalDocs)); @@ -221,6 +222,28 @@ internal void Walk(OpenApiPaths paths) } } + /// + /// Visits Webhooks and child objects + /// + internal void Walk(IDictionary webhooks) + { + if (webhooks == null) + { + return; + } + + _visitor.Visit(webhooks); + + // Visit Webhooks + if (webhooks != null) + { + foreach (var pathItem in webhooks) + { + Walk(pathItem.Key, () => Walk(pathItem.Value)); + } + } + } + /// /// Visits list of and child objects /// From 7a145758c29cb2e52a36730a94c61032a132b2f4 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 24 Oct 2022 13:23:53 +0300 Subject: [PATCH 10/21] Update the validation rule to exclude paths as a required field according to the 3.1 spec --- .../Validations/Rules/OpenApiDocumentRules.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index e5193b4c2..7f468f59a 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -28,15 +28,6 @@ public static class OpenApiDocumentRules String.Format(SRResource.Validation_FieldIsRequired, "info", "document")); } context.Exit(); - - // paths - context.Enter("paths"); - if (item.Paths == null) - { - context.CreateError(nameof(OpenApiDocumentFieldIsMissing), - String.Format(SRResource.Validation_FieldIsRequired, "paths", "document")); - } - context.Exit(); }); } } From 8d004ffeacef5dbc32bb403cc0978364010a5b5b Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Tue, 25 Oct 2022 11:11:42 +0300 Subject: [PATCH 11/21] Revert change --- .../Validations/Rules/OpenApiDocumentRules.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index 7f468f59a..00ee36a7d 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -28,6 +28,15 @@ public static class OpenApiDocumentRules String.Format(SRResource.Validation_FieldIsRequired, "info", "document")); } context.Exit(); + + // paths + context.Enter("paths"); + if (item.Paths == null) + { + context.CreateError(nameof(OpenApiDocumentFieldIsMissing), + String.Format(SRResource.Validation_FieldIsRequired, "paths", "document")); + } + context.Exit(); }); } } From 149175cad5f838a4e062c6d7c9c10605b96808d8 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Tue, 25 Oct 2022 11:40:08 +0300 Subject: [PATCH 12/21] Update test with correct property type --- .../V3Tests/OpenApiDocumentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index b31e1a6ce..ef25ae21c 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -1429,7 +1429,7 @@ public void ParseDocumentWithWebhooksShouldSucceed() Version = "1.0.0", Title = "Webhook Example" }, - Webhooks = new OpenApiPaths + Webhooks = new Dictionary { ["/pets"] = new OpenApiPathItem { From 17b1c2dc0f503962abe90d1f2772ad577bfbf068 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Wed, 26 Oct 2022 12:52:54 +0300 Subject: [PATCH 13/21] Add the validation for Paths as a required field in 3.0 during parsing --- .../V3/OpenApiDocumentDeserializer.cs | 14 +++++++++++++- .../Validations/Rules/OpenApiDocumentRules.cs | 9 --------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs index 33a9f706a..95a32294d 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs @@ -48,12 +48,24 @@ internal static partial class OpenApiV3Deserializer public static OpenApiDocument LoadOpenApi(RootNode rootNode) { var openApidoc = new OpenApiDocument(); - + var openApiNode = rootNode.GetMap(); ParseMap(openApiNode, openApidoc, _openApiFixedFields, _openApiPatternFields); + ValidatePathsField(openApidoc, rootNode); return openApidoc; } + + private static void ValidatePathsField(OpenApiDocument doc, RootNode rootNode) + { + var versionNode = rootNode.Find(new JsonPointer("/openapi")).GetScalarValue(); + if (versionNode == null) return; + else if (versionNode.Contains("3.0") && doc.Paths == null) + { + // paths is a required field in OpenAPI 3.0 but optional in 3.1 + rootNode.Context.Diagnostic.Errors.Add(new OpenApiError("", $"Paths is a REQUIRED field at {rootNode.Context.GetLocation()}")); + } + } } } diff --git a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs index 00ee36a7d..7f468f59a 100644 --- a/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs +++ b/src/Microsoft.OpenApi/Validations/Rules/OpenApiDocumentRules.cs @@ -28,15 +28,6 @@ public static class OpenApiDocumentRules String.Format(SRResource.Validation_FieldIsRequired, "info", "document")); } context.Exit(); - - // paths - context.Enter("paths"); - if (item.Paths == null) - { - context.CreateError(nameof(OpenApiDocumentFieldIsMissing), - String.Format(SRResource.Validation_FieldIsRequired, "paths", "document")); - } - context.Exit(); }); } } From c79bd11bb594c1610b2b4d76746b23dcd4c7ab3d Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Wed, 26 Oct 2022 12:53:07 +0300 Subject: [PATCH 14/21] Update spec version --- .../V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml index 11855036d..189835344 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/Samples/OpenApiDocument/documentWithWebhooks.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.1 +openapi: 3.1.0 info: title: Webhook Example version: 1.0.0 From b7e3e48bcc11138735754a74230e3b84af14b866 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Wed, 26 Oct 2022 17:37:21 +0300 Subject: [PATCH 15/21] Add more validation for empty paths and missing paths/webhooks for 3.1 --- .../ParsingContext.cs | 18 ++++++++++++++++++ .../V3/OpenApiDocumentDeserializer.cs | 12 ------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs index 659f053c6..c52741f65 100644 --- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs +++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs @@ -63,12 +63,14 @@ internal OpenApiDocument Parse(YamlDocument yamlDocument) VersionService = new OpenApiV2VersionService(Diagnostic); doc = VersionService.LoadDocument(RootNode); this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0; + ValidateRequiredFields(doc, version); break; case string version when version.StartsWith("3.0") || version.StartsWith("3.1"): VersionService = new OpenApiV3VersionService(Diagnostic); doc = VersionService.LoadDocument(RootNode); this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_0; + ValidateRequiredFields(doc, version); break; default: @@ -244,5 +246,21 @@ public void PopLoop(string loopid) } } + private void ValidateRequiredFields(OpenApiDocument doc, string version) + { + if ((version == "2.0" || version.StartsWith("3.0")) && (doc.Paths == null || doc.Paths.Count == 0)) + { + // paths is a required field in OpenAPI 3.0 but optional in 3.1 + RootNode.Context.Diagnostic.Errors.Add(new OpenApiError("", $"Paths is a REQUIRED field at {RootNode.Context.GetLocation()}")); + } + else if (version.StartsWith("3.1")) + { + if ((doc.Paths == null || doc.Paths.Count == 0) && (doc.Webhooks == null || doc.Webhooks.Count == 0)) + { + RootNode.Context.Diagnostic.Errors.Add(new OpenApiError( + "", $"The document MUST contain either a Paths or Webhooks field at {RootNode.Context.GetLocation()}")); + } + } + } } } diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs index 95a32294d..0d6b1f9aa 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs @@ -52,20 +52,8 @@ public static OpenApiDocument LoadOpenApi(RootNode rootNode) var openApiNode = rootNode.GetMap(); ParseMap(openApiNode, openApidoc, _openApiFixedFields, _openApiPatternFields); - ValidatePathsField(openApidoc, rootNode); return openApidoc; } - - private static void ValidatePathsField(OpenApiDocument doc, RootNode rootNode) - { - var versionNode = rootNode.Find(new JsonPointer("/openapi")).GetScalarValue(); - if (versionNode == null) return; - else if (versionNode.Contains("3.0") && doc.Paths == null) - { - // paths is a required field in OpenAPI 3.0 but optional in 3.1 - rootNode.Context.Diagnostic.Errors.Add(new OpenApiError("", $"Paths is a REQUIRED field at {rootNode.Context.GetLocation()}")); - } - } } } From 846fb0e3b3c2fd807ec887416c5d84246b285095 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Thu, 27 Oct 2022 09:10:36 +0300 Subject: [PATCH 16/21] Use Any() instead of count --- src/Microsoft.OpenApi.Readers/ParsingContext.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs index c52741f65..2a8f7399d 100644 --- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs +++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs @@ -248,14 +248,14 @@ public void PopLoop(string loopid) private void ValidateRequiredFields(OpenApiDocument doc, string version) { - if ((version == "2.0" || version.StartsWith("3.0")) && (doc.Paths == null || doc.Paths.Count == 0)) + if ((version == "2.0" || version.StartsWith("3.0")) && (doc.Paths == null || doc.Paths.Any())) { // paths is a required field in OpenAPI 3.0 but optional in 3.1 RootNode.Context.Diagnostic.Errors.Add(new OpenApiError("", $"Paths is a REQUIRED field at {RootNode.Context.GetLocation()}")); } else if (version.StartsWith("3.1")) { - if ((doc.Paths == null || doc.Paths.Count == 0) && (doc.Webhooks == null || doc.Webhooks.Count == 0)) + if ((doc.Paths == null || doc.Paths.Count == 0) && (doc.Webhooks == null || doc.Webhooks.Any())) { RootNode.Context.Diagnostic.Errors.Add(new OpenApiError( "", $"The document MUST contain either a Paths or Webhooks field at {RootNode.Context.GetLocation()}")); From 229911725685c1b5b5d5b750b35a922f0eca6a0e Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Thu, 27 Oct 2022 11:32:55 +0300 Subject: [PATCH 17/21] Code clean up --- src/Microsoft.OpenApi.Readers/ParsingContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs index 2a8f7399d..6a538c15e 100644 --- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs +++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs @@ -255,7 +255,7 @@ private void ValidateRequiredFields(OpenApiDocument doc, string version) } else if (version.StartsWith("3.1")) { - if ((doc.Paths == null || doc.Paths.Count == 0) && (doc.Webhooks == null || doc.Webhooks.Any())) + if ((doc.Paths == null || doc.Paths.Any()) && (doc.Webhooks == null || doc.Webhooks.Any())) { RootNode.Context.Diagnostic.Errors.Add(new OpenApiError( "", $"The document MUST contain either a Paths or Webhooks field at {RootNode.Context.GetLocation()}")); From 21e19a093e584b54f4d477f3dcd34a2605fccb97 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Thu, 27 Oct 2022 11:41:00 +0300 Subject: [PATCH 18/21] Add negation operator --- src/Microsoft.OpenApi.Readers/ParsingContext.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs index 6a538c15e..ac1e2a497 100644 --- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs +++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs @@ -248,14 +248,14 @@ public void PopLoop(string loopid) private void ValidateRequiredFields(OpenApiDocument doc, string version) { - if ((version == "2.0" || version.StartsWith("3.0")) && (doc.Paths == null || doc.Paths.Any())) + if ((version == "2.0" || version.StartsWith("3.0")) && (doc.Paths == null || !doc.Paths.Any())) { // paths is a required field in OpenAPI 3.0 but optional in 3.1 RootNode.Context.Diagnostic.Errors.Add(new OpenApiError("", $"Paths is a REQUIRED field at {RootNode.Context.GetLocation()}")); } else if (version.StartsWith("3.1")) { - if ((doc.Paths == null || doc.Paths.Any()) && (doc.Webhooks == null || doc.Webhooks.Any())) + if ((doc.Paths == null || !doc.Paths.Any()) && (doc.Webhooks == null || !doc.Webhooks.Any())) { RootNode.Context.Diagnostic.Errors.Add(new OpenApiError( "", $"The document MUST contain either a Paths or Webhooks field at {RootNode.Context.GetLocation()}")); From 615233690469d5c49c1e4ea524783cf927fd1f59 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Thu, 27 Oct 2022 15:48:59 +0300 Subject: [PATCH 19/21] Change reference type to pathItem --- src/Microsoft.OpenApi/Models/OpenApiDocument.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs index 5a73bc8a0..9544550b5 100644 --- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs +++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs @@ -130,7 +130,7 @@ public void SerializeAsV3(IOpenApiWriter writer) (w, key, component) => { if (component.Reference != null && - component.Reference.Type == ReferenceType.Schema && + component.Reference.Type == ReferenceType.PathItem && component.Reference.Id == key) { component.SerializeAsV3WithoutReference(w); From 86594a88186a618f3ea48a5e5ace9b1d1c2349af Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 31 Oct 2022 15:36:35 +0300 Subject: [PATCH 20/21] Reuse LoadPaths() logic to avoid creating a root reference object in webhooks --- src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs index 0d6b1f9aa..4857251da 100644 --- a/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs +++ b/src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs @@ -23,7 +23,7 @@ internal static partial class OpenApiV3Deserializer {"info", (o, n) => o.Info = LoadInfo(n)}, {"servers", (o, n) => o.Servers = n.CreateList(LoadServer)}, {"paths", (o, n) => o.Paths = LoadPaths(n)}, - {"webhooks", (o, n) => o.Webhooks = n.CreateMapWithReference(ReferenceType.PathItem, LoadPathItem)}, + {"webhooks", (o, n) => o.Webhooks = LoadPaths(n)}, {"components", (o, n) => o.Components = LoadComponents(n)}, {"tags", (o, n) => {o.Tags = n.CreateList(LoadTag); foreach (var tag in o.Tags) From 243eb23ed6e67804b64a9b82c50d54b35094ef78 Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Mon, 31 Oct 2022 15:36:45 +0300 Subject: [PATCH 21/21] Update test --- .../V3Tests/OpenApiDocumentTests.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs index ef25ae21c..85922f993 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs @@ -1525,11 +1525,6 @@ public void ParseDocumentWithWebhooksShouldSucceed() } } } - }, - Reference = new OpenApiReference - { - Type = ReferenceType.PathItem, - Id = "/pets" } } },