Skip to content

Commit 396779e

Browse files
authored
Merge pull request #34 from BinkyLabs/feat/5-new-fields-path-item
feat: add new v32 properties for Path Items (query, additional operations)
2 parents fd0c76a + 49f9560 commit 396779e

File tree

12 files changed

+962
-14
lines changed

12 files changed

+962
-14
lines changed

src/Microsoft.OpenApi/Models/Interfaces/IOpenApiPathItem.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-

2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.Net.Http;
43

54
namespace Microsoft.OpenApi;

src/Microsoft.OpenApi/Models/OpenApiConstants.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,11 @@ public static class OpenApiConstants
792792
/// </summary>
793793
public static readonly Version version2_0 = new(2, 0);
794794

795+
/// <summary>
796+
/// Field: AdditionalOperations
797+
/// </summary>
798+
public const string AdditionalOperations = "additionalOperations";
799+
795800
/// <summary>
796801
/// Field: BasePath
797802
/// </summary>

src/Microsoft.OpenApi/Models/OpenApiPathItem.cs

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Net.Http;
78

89
namespace Microsoft.OpenApi
@@ -97,15 +98,18 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
9798
// operations except "trace"
9899
if (Operations != null)
99100
{
100-
foreach (var operation in Operations)
101+
102+
foreach (var operation in Operations.Where(o => _standardHttp2MethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)))
101103
{
102-
if (operation.Key != HttpMethod.Trace)
103-
{
104-
writer.WriteOptionalObject(
105-
operation.Key.Method.ToLowerInvariant(),
106-
operation.Value,
107-
(w, o) => o.SerializeAsV2(w));
108-
}
104+
writer.WriteOptionalObject(
105+
operation.Key.Method.ToLowerInvariant(),
106+
operation.Value,
107+
(w, o) => o.SerializeAsV2(w));
108+
}
109+
var nonStandardOperations = Operations.Where(o => !_standardHttp2MethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)).ToDictionary(static o => o.Key.Method, static o => o.Value);
110+
if (nonStandardOperations.Count > 0)
111+
{
112+
writer.WriteRequiredMap($"x-oai-{OpenApiConstants.AdditionalOperations}", nonStandardOperations, (w, o) => o.SerializeAsV2(w));
109113
}
110114
}
111115

@@ -126,6 +130,31 @@ public virtual void SerializeAsV2(IOpenApiWriter writer)
126130
writer.WriteEndObject();
127131
}
128132

133+
internal static readonly HashSet<string> _standardHttp2MethodsNames = new(StringComparer.OrdinalIgnoreCase)
134+
{
135+
"get",
136+
"put",
137+
"post",
138+
"delete",
139+
"options",
140+
"head",
141+
"patch",
142+
};
143+
144+
internal static readonly HashSet<string> _standardHttp30MethodsNames = new(_standardHttp2MethodsNames, StringComparer.OrdinalIgnoreCase)
145+
{
146+
"trace",
147+
};
148+
149+
internal static readonly HashSet<string> _standardHttp31MethodsNames = new(_standardHttp30MethodsNames, StringComparer.OrdinalIgnoreCase)
150+
{
151+
};
152+
153+
internal static readonly HashSet<string> _standardHttp32MethodsNames = new(_standardHttp31MethodsNames, StringComparer.OrdinalIgnoreCase)
154+
{
155+
"query",
156+
};
157+
129158
internal virtual void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
130159
Action<IOpenApiWriter, IOpenApiSerializable> callback)
131160
{
@@ -139,16 +168,35 @@ internal virtual void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersio
139168
// description
140169
writer.WriteProperty(OpenApiConstants.Description, Description);
141170

171+
var standardMethodsNames = version switch
172+
{
173+
OpenApiSpecVersion.OpenApi2_0 => _standardHttp2MethodsNames,
174+
OpenApiSpecVersion.OpenApi3_0 => _standardHttp30MethodsNames,
175+
OpenApiSpecVersion.OpenApi3_1 => _standardHttp31MethodsNames,
176+
OpenApiSpecVersion.OpenApi3_2 or _ => _standardHttp32MethodsNames,
177+
};
178+
142179
// operations
143180
if (Operations != null)
144181
{
145-
foreach (var operation in Operations)
182+
foreach (var operation in Operations.Where(o => standardMethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)))
146183
{
147184
writer.WriteOptionalObject(
148185
operation.Key.Method.ToLowerInvariant(),
149186
operation.Value,
150187
callback);
151188
}
189+
var nonStandardOperations = Operations.Where(o => !standardMethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)).ToDictionary(static o => o.Key.Method, static o => o.Value);
190+
if (nonStandardOperations.Count > 0)
191+
{
192+
var additionalOperationsPropertyName = version switch
193+
{
194+
OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0 or OpenApiSpecVersion.OpenApi3_1 =>
195+
$"x-oai-{OpenApiConstants.AdditionalOperations}",
196+
_ => OpenApiConstants.AdditionalOperations,
197+
};
198+
writer.WriteRequiredMap(additionalOperationsPropertyName, nonStandardOperations, (w, o) => o.SerializeAsV32(w));
199+
}
152200
}
153201

154202
// servers

src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Net.Http;
35

46
namespace Microsoft.OpenApi.Reader.V32
@@ -35,11 +37,33 @@ internal static partial class OpenApiV32Deserializer
3537
#else
3638
{"patch", (o, n, t) => o.AddOperation(new HttpMethod("PATCH"), LoadOperation(n, t))},
3739
#endif
40+
{"query", (o, n, t) => o.AddOperation(new HttpMethod("QUERY"), LoadOperation(n, t))},
3841
{"trace", (o, n, t) => o.AddOperation(HttpMethod.Trace, LoadOperation(n, t))},
3942
{"servers", (o, n, t) => o.Servers = n.CreateList(LoadServer, t)},
40-
{"parameters", (o, n, t) => o.Parameters = n.CreateList(LoadParameter, t)}
43+
{"parameters", (o, n, t) => o.Parameters = n.CreateList(LoadParameter, t)},
44+
{OpenApiConstants.AdditionalOperations, LoadAdditionalOperations }
4145
};
4246

47+
48+
49+
private static void LoadAdditionalOperations(OpenApiPathItem o, ParseNode n, OpenApiDocument t)
50+
{
51+
if (n is null)
52+
{
53+
return;
54+
}
55+
56+
var mapNode = n.CheckMapNode(OpenApiConstants.AdditionalOperations);
57+
58+
foreach (var property in mapNode.Where(p => !OpenApiPathItem._standardHttp32MethodsNames.Contains(p.Name)))
59+
{
60+
var operationType = property.Name;
61+
62+
var httpMethod = new HttpMethod(operationType);
63+
o.AddOperation(httpMethod, LoadOperation(property.Value, t));
64+
}
65+
}
66+
4367
private static readonly PatternFieldMap<OpenApiPathItem> _pathItemPatternFields =
4468
new()
4569
{
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Net.Http;
6+
using System.Text.Json.Nodes;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
10+
namespace Microsoft.OpenApi.Readers.Tests.V31Tests;
11+
12+
public class OpenApiPathItemDeserializerTests
13+
{
14+
private const string SampleFolderPath = "V31Tests/Samples/OpenApiPathItem/";
15+
16+
[Fact]
17+
public async Task ExtraneousOperationsAreParsedAsExtensionsIn31()
18+
{
19+
// Arrange & Act
20+
var result = await OpenApiDocument.LoadAsync($"{SampleFolderPath}pathItemWithQueryAndAdditionalOperations.yaml", SettingsFixture.ReaderSettings);
21+
var pathItem = result.Document.Paths["/pets"];
22+
23+
// Assert
24+
Assert.Equal("Pet operations", pathItem.Summary);
25+
Assert.Equal("Operations available for pets", pathItem.Description);
26+
27+
// Regular operations
28+
var getOp = Assert.Contains(HttpMethod.Get, pathItem.Operations);
29+
Assert.Equal("getPets", getOp.OperationId);
30+
31+
var postOp = Assert.Contains(HttpMethod.Post, pathItem.Operations);
32+
Assert.Equal("createPet", postOp.OperationId);
33+
34+
// Query operation should now be on one of the operations
35+
// Since the YAML structure changed, we need to check which operation has the query
36+
Assert.DoesNotContain(new HttpMethod("Query"), pathItem.Operations);
37+
Assert.DoesNotContain(new HttpMethod("notify"), pathItem.Operations);
38+
Assert.DoesNotContain(new HttpMethod("subscribe"), pathItem.Operations);
39+
40+
var additionalPathsExt = Assert.IsType<JsonNodeExtension>(Assert.Contains("x-oai-additionalOperations", pathItem.Extensions));
41+
42+
var additionalOpsNode = Assert.IsType<JsonObject>(additionalPathsExt.Node);
43+
Assert.Contains("query", additionalOpsNode);
44+
Assert.Contains("notify", additionalOpsNode);
45+
Assert.Contains("subscribe", additionalOpsNode);
46+
}
47+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
openapi: 3.1.0
2+
info:
3+
title: PathItem with Query and AdditionalOperations
4+
version: 1.0.0
5+
paths:
6+
/pets:
7+
summary: Pet operations
8+
description: Operations available for pets
9+
get:
10+
summary: Get pets
11+
operationId: getPets
12+
responses:
13+
'200':
14+
description: List of pets
15+
x-oai-additionalOperations:
16+
query:
17+
summary: Query pets with complex filters
18+
operationId: queryPets
19+
parameters:
20+
- name: filter
21+
in: query
22+
description: Complex filter expression
23+
schema:
24+
type: string
25+
responses:
26+
'200':
27+
description: Filtered pets
28+
content:
29+
application/json:
30+
schema:
31+
type: array
32+
items:
33+
type: object
34+
notify:
35+
summary: Notify about pet updates
36+
operationId: notifyPetUpdates
37+
requestBody:
38+
required: true
39+
content:
40+
application/json:
41+
schema:
42+
type: object
43+
properties:
44+
petId:
45+
type: string
46+
event:
47+
type: string
48+
responses:
49+
'200':
50+
description: Notification sent
51+
subscribe:
52+
summary: Subscribe to pet events
53+
operationId: subscribePetEvents
54+
parameters:
55+
- name: events
56+
in: query
57+
description: Event types to subscribe to
58+
schema:
59+
type: array
60+
items:
61+
type: string
62+
responses:
63+
'200':
64+
description: Subscription created
65+
content:
66+
application/json:
67+
schema:
68+
type: object
69+
properties:
70+
subscriptionId:
71+
type: string
72+
post:
73+
summary: Create pet
74+
operationId: createPet
75+
responses:
76+
'201':
77+
description: Pet created
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Net.Http;
6+
using System.Threading.Tasks;
7+
using Xunit;
8+
9+
namespace Microsoft.OpenApi.Readers.Tests.V32Tests;
10+
11+
public class OpenApiPathItemDeserializerTests
12+
{
13+
private const string SampleFolderPath = "V32Tests/Samples/OpenApiPathItem/";
14+
15+
[Fact]
16+
public async Task ParsePathItemWithQueryAndAdditionalOperationsV32Works()
17+
{
18+
// Arrange & Act
19+
var result = await OpenApiDocument.LoadAsync($"{SampleFolderPath}pathItemWithQueryAndAdditionalOperations.yaml", SettingsFixture.ReaderSettings);
20+
var pathItem = result.Document.Paths["/pets"];
21+
22+
// Assert
23+
Assert.Equal("Pet operations", pathItem.Summary);
24+
Assert.Equal("Operations available for pets", pathItem.Description);
25+
26+
// Regular operations
27+
var getOp = Assert.Contains(HttpMethod.Get, pathItem.Operations);
28+
Assert.Equal("getPets", getOp.OperationId);
29+
30+
var postOp = Assert.Contains(HttpMethod.Post, pathItem.Operations);
31+
Assert.Equal("createPet", postOp.OperationId);
32+
33+
// Query operation should now be on one of the operations
34+
// Since the YAML structure changed, we need to check which operation has the query
35+
var queryOp = Assert.Contains(new HttpMethod("Query"), pathItem.Operations);
36+
Assert.Equal("Query pets with complex filters", queryOp.Summary);
37+
Assert.Equal("queryPets", queryOp.OperationId);
38+
Assert.Single(queryOp.Parameters);
39+
Assert.Equal("filter", queryOp.Parameters[0].Name);
40+
41+
var notifyOp = Assert.Contains(new HttpMethod("notify"), pathItem.Operations);
42+
Assert.Equal("Notify about pet updates", notifyOp.Summary);
43+
Assert.Equal("notifyPetUpdates", notifyOp.OperationId);
44+
Assert.NotNull(notifyOp.RequestBody);
45+
46+
var subscribeOp = Assert.Contains(new HttpMethod("subscribe"), pathItem.Operations);
47+
Assert.Equal("Subscribe to pet events", subscribeOp.Summary);
48+
Assert.Equal("subscribePetEvents", subscribeOp.OperationId);
49+
Assert.Single(subscribeOp.Parameters);
50+
Assert.Equal("events", subscribeOp.Parameters[0].Name);
51+
}
52+
}

0 commit comments

Comments
 (0)