Skip to content

Commit 3db2ffc

Browse files
Merge pull request #1056 from microsoft/mk/add-pathItems-to-components-object
Add reusable path items to components object
2 parents 7472db9 + 1d35f0b commit 3db2ffc

27 files changed

+1320
-110
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
6+
namespace Microsoft.OpenApi.Readers
7+
{
8+
/// <summary>
9+
/// Generates custom extension methods for the version string type
10+
/// </summary>
11+
public static class OpenApiVersionExtensionMethods
12+
{
13+
/// <summary>
14+
/// Extension method for Spec version 2.0
15+
/// </summary>
16+
/// <param name="version"></param>
17+
/// <returns></returns>
18+
public static bool is2_0(this string version)
19+
{
20+
bool result = false;
21+
if (version.Equals("2.0", StringComparison.OrdinalIgnoreCase))
22+
{
23+
result = true;
24+
}
25+
26+
return result;
27+
}
28+
29+
/// <summary>
30+
/// Extension method for Spec version 3.0
31+
/// </summary>
32+
/// <param name="version"></param>
33+
/// <returns></returns>
34+
public static bool is3_0(this string version)
35+
{
36+
bool result = false;
37+
if (version.StartsWith("3.0", StringComparison.OrdinalIgnoreCase))
38+
{
39+
result = true;
40+
}
41+
42+
return result;
43+
}
44+
45+
/// <summary>
46+
/// Extension method for Spec version 3.1
47+
/// </summary>
48+
/// <param name="version"></param>
49+
/// <returns></returns>
50+
public static bool is3_1(this string version)
51+
{
52+
bool result = false;
53+
if (version.StartsWith("3.1", StringComparison.OrdinalIgnoreCase))
54+
{
55+
result = true;
56+
}
57+
58+
return result;
59+
}
60+
}
61+
}

src/Microsoft.OpenApi.Readers/ParsingContext.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,18 @@ internal OpenApiDocument Parse(YamlDocument yamlDocument)
5959

6060
switch (inputVersion)
6161
{
62-
case string version when version == "2.0":
62+
case string version when version.is2_0():
6363
VersionService = new OpenApiV2VersionService(Diagnostic);
6464
doc = VersionService.LoadDocument(RootNode);
6565
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi2_0;
66+
ValidateRequiredFields(doc, version);
6667
break;
6768

68-
case string version when version.StartsWith("3.0"):
69+
case string version when version.is3_0() || version.is3_1():
6970
VersionService = new OpenApiV3VersionService(Diagnostic);
7071
doc = VersionService.LoadDocument(RootNode);
7172
this.Diagnostic.SpecificationVersion = OpenApiSpecVersion.OpenApi3_0;
73+
ValidateRequiredFields(doc, version);
7274
break;
7375

7476
default:
@@ -244,5 +246,18 @@ public void PopLoop(string loopid)
244246
}
245247
}
246248

249+
private void ValidateRequiredFields(OpenApiDocument doc, string version)
250+
{
251+
if ((version.is2_0() || version.is3_0()) && (doc.Paths == null || !doc.Paths.Any()))
252+
{
253+
// paths is a required field in OpenAPI 3.0 but optional in 3.1
254+
RootNode.Context.Diagnostic.Errors.Add(new OpenApiError("", $"Paths is a REQUIRED field at {RootNode.Context.GetLocation()}"));
255+
}
256+
else if (version.is3_1() && (doc.Paths == null || !doc.Paths.Any()) && (doc.Webhooks == null || !doc.Webhooks.Any()))
257+
{
258+
RootNode.Context.Diagnostic.Errors.Add(new OpenApiError(
259+
"", $"The document MUST contain either a Paths or Webhooks field at {RootNode.Context.GetLocation()}"));
260+
}
261+
}
247262
}
248263
}

src/Microsoft.OpenApi.Readers/V3/OpenApiComponentsDeserializer.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ internal static partial class OpenApiV3Deserializer
1717
{
1818
private static FixedFieldMap<OpenApiComponents> _componentsFixedFields = new FixedFieldMap<OpenApiComponents>
1919
{
20-
{
21-
"schemas", (o, n) => o.Schemas = n.CreateMapWithReference(ReferenceType.Schema, LoadSchema)
22-
},
20+
{"schemas", (o, n) => o.Schemas = n.CreateMapWithReference(ReferenceType.Schema, LoadSchema)},
2321
{"responses", (o, n) => o.Responses = n.CreateMapWithReference(ReferenceType.Response, LoadResponse)},
2422
{"parameters", (o, n) => o.Parameters = n.CreateMapWithReference(ReferenceType.Parameter, LoadParameter)},
2523
{"examples", (o, n) => o.Examples = n.CreateMapWithReference(ReferenceType.Example, LoadExample)},
@@ -28,9 +26,9 @@ internal static partial class OpenApiV3Deserializer
2826
{"securitySchemes", (o, n) => o.SecuritySchemes = n.CreateMapWithReference(ReferenceType.SecurityScheme, LoadSecurityScheme)},
2927
{"links", (o, n) => o.Links = n.CreateMapWithReference(ReferenceType.Link, LoadLink)},
3028
{"callbacks", (o, n) => o.Callbacks = n.CreateMapWithReference(ReferenceType.Callback, LoadCallback)},
29+
{"pathItems", (o, n) => o.PathItems = n.CreateMapWithReference(ReferenceType.PathItem, LoadPathItem)}
3130
};
3231

33-
3432
private static PatternFieldMap<OpenApiComponents> _componentsPatternFields =
3533
new PatternFieldMap<OpenApiComponents>
3634
{

src/Microsoft.OpenApi.Readers/V3/OpenApiDocumentDeserializer.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4-
using System.Collections.Generic;
5-
using Microsoft.OpenApi.Any;
64
using Microsoft.OpenApi.Extensions;
7-
using Microsoft.OpenApi.Interfaces;
85
using Microsoft.OpenApi.Models;
96
using Microsoft.OpenApi.Readers.ParseNodes;
107

@@ -26,6 +23,7 @@ internal static partial class OpenApiV3Deserializer
2623
{"info", (o, n) => o.Info = LoadInfo(n)},
2724
{"servers", (o, n) => o.Servers = n.CreateList(LoadServer)},
2825
{"paths", (o, n) => o.Paths = LoadPaths(n)},
26+
{"webhooks", (o, n) => o.Webhooks = LoadPaths(n)},
2927
{"components", (o, n) => o.Components = LoadComponents(n)},
3028
{"tags", (o, n) => {o.Tags = n.CreateList(LoadTag);
3129
foreach (var tag in o.Tags)
@@ -50,7 +48,6 @@ internal static partial class OpenApiV3Deserializer
5048
public static OpenApiDocument LoadOpenApi(RootNode rootNode)
5149
{
5250
var openApidoc = new OpenApiDocument();
53-
5451
var openApiNode = rootNode.GetMap();
5552

5653
ParseMap(openApiNode, openApidoc, _openApiFixedFields, _openApiPatternFields);

src/Microsoft.OpenApi.Readers/V3/OpenApiPathItemDeserializer.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ public static OpenApiPathItem LoadPathItem(ParseNode node)
5656
{
5757
var mapNode = node.CheckMapNode("PathItem");
5858

59+
var pointer = mapNode.GetReferencePointer();
60+
61+
if (pointer != null)
62+
{
63+
return new OpenApiPathItem()
64+
{
65+
UnresolvedReference = true,
66+
Reference = node.Context.VersionService.ConvertToOpenApiReference(pointer, ReferenceType.PathItem)
67+
};
68+
}
69+
5970
var pathItem = new OpenApiPathItem();
6071

6172
ParseMap(mapNode, pathItem, _pathItemFixedFields, _pathItemPatternFields);

src/Microsoft.OpenApi.Readers/V3/OpenApiV3VersionService.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,12 @@ private OpenApiReference ParseLocalReference(string localReference)
165165
if (segments[1] == "components")
166166
{
167167
var referenceType = segments[2].GetEnumFromDisplayName<ReferenceType>();
168-
return new OpenApiReference { Type = referenceType, Id = segments[3] };
168+
var refId = segments[3];
169+
if (segments[2] == "pathItems")
170+
{
171+
refId = "/" + segments[3];
172+
};
173+
return new OpenApiReference { Type = referenceType, Id = refId };
169174
}
170175
}
171176

src/Microsoft.OpenApi/Models/OpenApiComponents.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33

4-
using System;
54
using System.Collections.Generic;
65
using System.Linq;
7-
using Microsoft.OpenApi.Any;
86
using Microsoft.OpenApi.Interfaces;
97
using Microsoft.OpenApi.Writers;
108

@@ -63,6 +61,11 @@ public class OpenApiComponents : IOpenApiSerializable, IOpenApiExtensible
6361
/// </summary>
6462
public IDictionary<string, OpenApiCallback> Callbacks { get; set; } = new Dictionary<string, OpenApiCallback>();
6563

64+
/// <summary>
65+
/// An object to hold reusable <see cref="OpenApiPathItem"/> Object.
66+
/// </summary>
67+
public IDictionary<string, OpenApiPathItem> PathItems { get; set; } = new Dictionary<string, OpenApiPathItem>();
68+
6669
/// <summary>
6770
/// This object MAY be extended with Specification Extensions.
6871
/// </summary>
@@ -87,6 +90,7 @@ public OpenApiComponents(OpenApiComponents components)
8790
SecuritySchemes = components?.SecuritySchemes != null ? new Dictionary<string, OpenApiSecurityScheme>(components.SecuritySchemes) : null;
8891
Links = components?.Links != null ? new Dictionary<string, OpenApiLink>(components.Links) : null;
8992
Callbacks = components?.Callbacks != null ? new Dictionary<string, OpenApiCallback>(components.Callbacks) : null;
93+
PathItems = components?.PathItems != null ? new Dictionary<string, OpenApiPathItem>(components.PathItems) : null;
9094
Extensions = components?.Extensions != null ? new Dictionary<string, IOpenApiExtension>(components.Extensions) : null;
9195
}
9296

@@ -288,7 +292,25 @@ public void SerializeAsV3(IOpenApiWriter writer)
288292
component.SerializeAsV3(w);
289293
}
290294
});
291-
295+
296+
// pathItems
297+
writer.WriteOptionalMap(
298+
OpenApiConstants.PathItems,
299+
PathItems,
300+
(w, key, component) =>
301+
{
302+
if (component.Reference != null &&
303+
component.Reference.Type == ReferenceType.Schema &&
304+
component.Reference.Id == key)
305+
{
306+
component.SerializeAsV3WithoutReference(w);
307+
}
308+
else
309+
{
310+
component.SerializeAsV3(w);
311+
}
312+
});
313+
292314
// extensions
293315
writer.WriteExtensions(Extensions, OpenApiSpecVersion.OpenApi3_0);
294316

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public static class OpenApiConstants
2020
/// </summary>
2121
public const string Info = "info";
2222

23+
/// <summary>
24+
/// Field: Webhooks
25+
/// </summary>
26+
public const string Webhooks = "webhooks";
27+
2328
/// <summary>
2429
/// Field: Title
2530
/// </summary>
@@ -75,6 +80,11 @@ public static class OpenApiConstants
7580
/// </summary>
7681
public const string Components = "components";
7782

83+
/// <summary>
84+
/// Field: PathItems
85+
/// </summary>
86+
public const string PathItems = "pathItems";
87+
7888
/// <summary>
7989
/// Field: Security
8090
/// </summary>

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ public class OpenApiDocument : IOpenApiSerializable, IOpenApiExtensible
3939
/// </summary>
4040
public OpenApiPaths Paths { get; set; }
4141

42+
/// <summary>
43+
/// The incoming webhooks that MAY be received as part of this API and that the API consumer MAY choose to implement.
44+
/// A map of requests initiated other than by an API call, for example by an out of band registration.
45+
/// 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
46+
/// </summary>
47+
public IDictionary<string, OpenApiPathItem> Webhooks { get; set; } = new Dictionary<string, OpenApiPathItem>();
48+
4249
/// <summary>
4350
/// An element to hold various schemas for the specification.
4451
/// </summary>
@@ -84,6 +91,7 @@ public OpenApiDocument(OpenApiDocument document)
8491
Info = document?.Info != null ? new(document?.Info) : null;
8592
Servers = document?.Servers != null ? new List<OpenApiServer>(document.Servers) : null;
8693
Paths = document?.Paths != null ? new(document?.Paths) : null;
94+
Webhooks = document?.Webhooks != null ? new Dictionary<string, OpenApiPathItem>(document.Webhooks) : null;
8795
Components = document?.Components != null ? new(document?.Components) : null;
8896
SecurityRequirements = document?.SecurityRequirements != null ? new List<OpenApiSecurityRequirement>(document.SecurityRequirements) : null;
8997
Tags = document?.Tags != null ? new List<OpenApiTag>(document.Tags) : null;
@@ -115,6 +123,24 @@ public void SerializeAsV3(IOpenApiWriter writer)
115123
// paths
116124
writer.WriteRequiredObject(OpenApiConstants.Paths, Paths, (w, p) => p.SerializeAsV3(w));
117125

126+
// webhooks
127+
writer.WriteOptionalMap(
128+
OpenApiConstants.Webhooks,
129+
Webhooks,
130+
(w, key, component) =>
131+
{
132+
if (component.Reference != null &&
133+
component.Reference.Type == ReferenceType.PathItem &&
134+
component.Reference.Id == key)
135+
{
136+
component.SerializeAsV3WithoutReference(w);
137+
}
138+
else
139+
{
140+
component.SerializeAsV3(w);
141+
}
142+
});
143+
118144
// components
119145
writer.WriteOptionalObject(OpenApiConstants.Components, Components, (w, c) => c.SerializeAsV3(w));
120146

@@ -479,7 +505,10 @@ internal IOpenApiReferenceable ResolveReference(OpenApiReference reference, bool
479505
{
480506
case ReferenceType.Schema:
481507
return this.Components.Schemas[reference.Id];
482-
508+
509+
case ReferenceType.PathItem:
510+
return this.Components.PathItems[reference.Id];
511+
483512
case ReferenceType.Response:
484513
return this.Components.Responses[reference.Id];
485514

src/Microsoft.OpenApi/Models/ReferenceType.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public enum ReferenceType
5858
/// <summary>
5959
/// Tags item.
6060
/// </summary>
61-
[Display("tags")] Tag
61+
[Display("tags")] Tag,
62+
63+
/// <summary>
64+
/// Path item.
65+
/// </summary>
66+
[Display("pathItems")] PathItem
6267
}
6368
}

0 commit comments

Comments
 (0)