Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add webhooks property to the root document #1046

Merged
merged 21 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f6cc9d8
Add webhooks property to OpenAPI document
MaggieKimani1 Oct 17, 2022
0678365
Deep copy the webhooks object in the copy constructor
MaggieKimani1 Oct 17, 2022
5bb8f44
Add serialization for the webhooks property
MaggieKimani1 Oct 17, 2022
7479780
Add logic to deserialize the webhooks property
MaggieKimani1 Oct 17, 2022
43e165c
Clean up project references
MaggieKimani1 Oct 17, 2022
9efc130
Add pathItem reference type and webhooks constant
MaggieKimani1 Oct 17, 2022
c79a4f9
Adds tests
MaggieKimani1 Oct 24, 2022
103f123
Adds 3.1 as a valid input OpenAPI version
MaggieKimani1 Oct 24, 2022
624bd0c
Adds a walker to visit the webhooks object and its child elements
MaggieKimani1 Oct 24, 2022
7a14575
Update the validation rule to exclude paths as a required field accor…
MaggieKimani1 Oct 24, 2022
8d004ff
Revert change
MaggieKimani1 Oct 25, 2022
149175c
Update test with correct property type
MaggieKimani1 Oct 25, 2022
17b1c2d
Add the validation for Paths as a required field in 3.0 during parsing
MaggieKimani1 Oct 26, 2022
c79bd11
Update spec version
MaggieKimani1 Oct 26, 2022
b7e3e48
Add more validation for empty paths and missing paths/webhooks for 3.1
MaggieKimani1 Oct 26, 2022
846fb0e
Use Any() instead of count
MaggieKimani1 Oct 27, 2022
2299117
Code clean up
MaggieKimani1 Oct 27, 2022
21e19a0
Add negation operator
MaggieKimani1 Oct 27, 2022
6152336
Change reference type to pathItem
MaggieKimani1 Oct 27, 2022
86594a8
Reuse LoadPaths() logic to avoid creating a root reference object in …
MaggieKimani1 Oct 31, 2022
243eb23
Update test
MaggieKimani1 Oct 31, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi.Readers/ParsingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// 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;

Expand All @@ -26,6 +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)},
{"components", (o, n) => o.Components = LoadComponents(n)},
{"tags", (o, n) => {o.Tags = n.CreateList(LoadTag);
foreach (var tag in o.Tags)
Expand Down
5 changes: 5 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public static class OpenApiConstants
/// </summary>
public const string Info = "info";

/// <summary>
/// Field: Webhooks
/// </summary>
public const string Webhooks = "webhooks";

/// <summary>
/// Field: Title
/// </summary>
Expand Down
26 changes: 26 additions & 0 deletions src/Microsoft.OpenApi/Models/OpenApiDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible
/// </summary>
public OpenApiPaths Paths { get; set; }

/// <summary>
/// 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
/// </summary>
public IDictionary<string, OpenApiPathItem> Webhooks { get; set; } = new Dictionary<string, OpenApiPathItem>();

/// <summary>
/// An element to hold various schemas for the specification.
/// </summary>
Expand Down Expand Up @@ -84,6 +91,7 @@ public OpenApiDocument(OpenApiDocument document)
Info = document?.Info != null ? new(document?.Info) : null;
Servers = document?.Servers != null ? new List<OpenApiServer>(document.Servers) : null;
Paths = document?.Paths != null ? new(document?.Paths) : null;
Webhooks = document?.Webhooks != null ? new Dictionary<string, OpenApiPathItem>(document.Webhooks) : null;
Components = document?.Components != null ? new(document?.Components) : null;
SecurityRequirements = document?.SecurityRequirements != null ? new List<OpenApiSecurityRequirement>(document.SecurityRequirements) : null;
Tags = document?.Tags != null ? new List<OpenApiTag>(document.Tags) : null;
Expand Down Expand Up @@ -115,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));

Expand Down
7 changes: 6 additions & 1 deletion src/Microsoft.OpenApi/Models/ReferenceType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ public enum ReferenceType
/// <summary>
/// Tags item.
/// </summary>
[Display("tags")] Tag
[Display("tags")] Tag,

/// <summary>
/// Path item.
/// </summary>
[Display("pathItem")] PathItem,
}
}
7 changes: 7 additions & 0 deletions src/Microsoft.OpenApi/Services/OpenApiVisitorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ public virtual void Visit(OpenApiPaths paths)
{
}

/// <summary>
/// Visits Webhooks>
/// </summary>
public virtual void Visit(IDictionary<string, OpenApiPathItem> webhooks)
{
}

/// <summary>
/// Visits <see cref="OpenApiPathItem"/>
/// </summary>
Expand Down
23 changes: 23 additions & 0 deletions src/Microsoft.OpenApi/Services/OpenApiWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -221,6 +222,28 @@ internal void Walk(OpenApiPaths paths)
}
}

/// <summary>
/// Visits Webhooks and child objects
/// </summary>
internal void Walk(IDictionary<string, OpenApiPathItem> webhooks)
{
if (webhooks == null)
{
return;
}

_visitor.Visit(webhooks);

// Visit Webhooks
if (webhooks != null)
{
foreach (var pathItem in webhooks)
{
Walk(pathItem.Key, () => Walk(pathItem.Value));
}
}
}

/// <summary>
/// Visits list of <see cref="OpenApiServer"/> and child objects
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@
<EmbeddedResource Include="V3Tests\Samples\OpenApiDocument\minimalDocument.yaml">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="V3Tests\Samples\OpenApiDocument\documentWithWebhooks.yaml">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="V3Tests\Samples\OpenApiDocument\apiWithFullHeaderComponent.yaml">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
Expand Down
213 changes: 213 additions & 0 deletions test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, OpenApiSchema>
{
["pet"] = new OpenApiSchema
{
Type = "object",
Required = new HashSet<string>
{
"id",
"name"
},
Properties = new Dictionary<string, OpenApiSchema>
{
["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<string>
{
"name"
},
Properties = new Dictionary<string, OpenApiSchema>
{
["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, OpenApiOperation>
{
[OperationType.Get] = new OpenApiOperation
{
Description = "Returns all pets from the system that the user has access to",
OperationId = "findPets",
Parameters = new List<OpenApiParameter>
{
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<string, OpenApiMediaType>
{
["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<string, OpenApiMediaType>
{
["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<string, OpenApiMediaType>
{
["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);
}
}
}
Loading