Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
93d8a47
add new v32 properties for Path Items
kilifu Oct 1, 2025
9f0d2c1
Merge branch 'feat/oai-3-2-support' into feat/5-new-fields-path-item
baywet Oct 2, 2025
1cac1a2
move query and AdditionalOperations to OpenApiOperation
kilifu Oct 3, 2025
e3ae554
Merge branch 'feat/5-new-fields-path-item' of https://github.com/Bink…
kilifu Oct 3, 2025
40a22f8
Merge branch 'feat/oai-3-2-support' into feat/5-new-fields-path-item
kilifu Oct 3, 2025
a256053
chore: reverts some undesired changes
baywet Oct 3, 2025
f7dc7ee
feat: adds support for additional operations in OAI 3.2
baywet Oct 3, 2025
d608cdc
chore: linting
baywet Oct 3, 2025
1a104d9
chore: fixes test files for path item additional operations
baywet Oct 3, 2025
c6b1b0e
chore: updates public export
baywet Oct 3, 2025
4801b06
chore: make new unit tests build
baywet Oct 3, 2025
0520443
chore: refactoring
baywet Oct 3, 2025
4f47916
tests: fixes additional operation test file
baywet Oct 3, 2025
71e51ea
tests: fixes test definitions
baywet Oct 3, 2025
cbc2934
fix: do not serialized extraneous operations in v2
baywet Oct 3, 2025
39f300d
tests: fixes unit tests for path item serialization
baywet Oct 3, 2025
30227ef
Merge branch 'feat/oai-3-2-support' into feat/5-new-fields-path-item
baywet Oct 3, 2025
fefec09
test: adds tests for 30 deserialization of path items with extra ops
baywet Oct 5, 2025
0781a89
chore: moves tests definitions to the right location
baywet Oct 5, 2025
49f9560
tests: adds deserialization tests for 3.1 path item extra operations
baywet Oct 5, 2025
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
3 changes: 1 addition & 2 deletions src/Microsoft.OpenApi/Models/Interfaces/IOpenApiPathItem.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

using System.Collections.Generic;
using System.Collections.Generic;
using System.Net.Http;

namespace Microsoft.OpenApi;
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 @@ -792,6 +792,11 @@ public static class OpenApiConstants
/// </summary>
public static readonly Version version2_0 = new(2, 0);

/// <summary>
/// Field: AdditionalOperations
/// </summary>
public const string AdditionalOperations = "additionalOperations";

/// <summary>
/// Field: BasePath
/// </summary>
Expand Down
66 changes: 57 additions & 9 deletions src/Microsoft.OpenApi/Models/OpenApiPathItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;

namespace Microsoft.OpenApi
Expand Down Expand Up @@ -97,15 +98,18 @@
// operations except "trace"
if (Operations != null)
{
foreach (var operation in Operations)

foreach (var operation in Operations.Where(o => _standardHttp2MethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)))
{
if (operation.Key != HttpMethod.Trace)
{
writer.WriteOptionalObject(
operation.Key.Method.ToLowerInvariant(),
operation.Value,
(w, o) => o.SerializeAsV2(w));
}
writer.WriteOptionalObject(
operation.Key.Method.ToLowerInvariant(),
operation.Value,
(w, o) => o.SerializeAsV2(w));
}
var nonStandardOperations = Operations.Where(o => !_standardHttp2MethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)).ToDictionary(static o => o.Key.Method, static o => o.Value);
if (nonStandardOperations.Count > 0)
{
writer.WriteRequiredMap($"x-oai-{OpenApiConstants.AdditionalOperations}", nonStandardOperations, (w, o) => o.SerializeAsV2(w));
}
}

Expand All @@ -126,6 +130,31 @@
writer.WriteEndObject();
}

internal static readonly HashSet<string> _standardHttp2MethodsNames = new(StringComparer.OrdinalIgnoreCase)
{
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
};

internal static readonly HashSet<string> _standardHttp30MethodsNames = new(_standardHttp2MethodsNames, StringComparer.OrdinalIgnoreCase)
{
"trace",
};

internal static readonly HashSet<string> _standardHttp31MethodsNames = new(_standardHttp30MethodsNames, StringComparer.OrdinalIgnoreCase)
{
};

internal static readonly HashSet<string> _standardHttp32MethodsNames = new(_standardHttp31MethodsNames, StringComparer.OrdinalIgnoreCase)
{
"query",
};

internal virtual void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version,
Action<IOpenApiWriter, IOpenApiSerializable> callback)
{
Expand All @@ -139,16 +168,35 @@
// description
writer.WriteProperty(OpenApiConstants.Description, Description);

var standardMethodsNames = version switch
{
OpenApiSpecVersion.OpenApi2_0 => _standardHttp2MethodsNames,
OpenApiSpecVersion.OpenApi3_0 => _standardHttp30MethodsNames,
OpenApiSpecVersion.OpenApi3_1 => _standardHttp31MethodsNames,
OpenApiSpecVersion.OpenApi3_2 or _ => _standardHttp32MethodsNames,
};

// operations
if (Operations != null)
{
foreach (var operation in Operations)
foreach (var operation in Operations.Where(o => standardMethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)))
{
writer.WriteOptionalObject(
operation.Key.Method.ToLowerInvariant(),
operation.Value,
callback);
}
var nonStandardOperations = Operations.Where(o => !standardMethodsNames.Contains(o.Key.Method, StringComparer.OrdinalIgnoreCase)).ToDictionary(static o => o.Key.Method, static o => o.Value);
if (nonStandardOperations.Count > 0)
{
var additionalOperationsPropertyName = version switch
{
OpenApiSpecVersion.OpenApi2_0 or OpenApiSpecVersion.OpenApi3_0 or OpenApiSpecVersion.OpenApi3_1 =>
$"x-oai-{OpenApiConstants.AdditionalOperations}",
_ => OpenApiConstants.AdditionalOperations,
};
writer.WriteRequiredMap(additionalOperationsPropertyName, nonStandardOperations, (w, o) => o.SerializeAsV32(w));
}
}

// servers
Expand Down
28 changes: 26 additions & 2 deletions src/Microsoft.OpenApi/Reader/V32/OpenApiPathItemDeserializer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;

namespace Microsoft.OpenApi.Reader.V32
Expand Down Expand Up @@ -35,11 +37,33 @@ internal static partial class OpenApiV32Deserializer
#else
{"patch", (o, n, t) => o.AddOperation(new HttpMethod("PATCH"), LoadOperation(n, t))},
#endif
{"query", (o, n, t) => o.AddOperation(new HttpMethod("QUERY"), LoadOperation(n, t))},
{"trace", (o, n, t) => o.AddOperation(HttpMethod.Trace, LoadOperation(n, t))},
{"servers", (o, n, t) => o.Servers = n.CreateList(LoadServer, t)},
{"parameters", (o, n, t) => o.Parameters = n.CreateList(LoadParameter, t)}
{"parameters", (o, n, t) => o.Parameters = n.CreateList(LoadParameter, t)},
{OpenApiConstants.AdditionalOperations, LoadAdditionalOperations }
};



private static void LoadAdditionalOperations(OpenApiPathItem o, ParseNode n, OpenApiDocument t)
{
if (n is null)
{
return;
}

var mapNode = n.CheckMapNode(OpenApiConstants.AdditionalOperations);

foreach (var property in mapNode.Where(p => !OpenApiPathItem._standardHttp32MethodsNames.Contains(p.Name)))
{
var operationType = property.Name;

var httpMethod = new HttpMethod(operationType);
o.AddOperation(httpMethod, LoadOperation(property.Value, t));
}
}

private static readonly PatternFieldMap<OpenApiPathItem> _pathItemPatternFields =
new()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.OpenApi.Readers.Tests.V31Tests;

public class OpenApiPathItemDeserializerTests
{
private const string SampleFolderPath = "V31Tests/Samples/OpenApiPathItem/";

[Fact]
public async Task ExtraneousOperationsAreParsedAsExtensionsIn31()
{
// Arrange & Act
var result = await OpenApiDocument.LoadAsync($"{SampleFolderPath}pathItemWithQueryAndAdditionalOperations.yaml", SettingsFixture.ReaderSettings);
var pathItem = result.Document.Paths["/pets"];

// Assert
Assert.Equal("Pet operations", pathItem.Summary);
Assert.Equal("Operations available for pets", pathItem.Description);

// Regular operations
var getOp = Assert.Contains(HttpMethod.Get, pathItem.Operations);
Assert.Equal("getPets", getOp.OperationId);

var postOp = Assert.Contains(HttpMethod.Post, pathItem.Operations);
Assert.Equal("createPet", postOp.OperationId);

// Query operation should now be on one of the operations
// Since the YAML structure changed, we need to check which operation has the query
Assert.DoesNotContain(new HttpMethod("Query"), pathItem.Operations);
Assert.DoesNotContain(new HttpMethod("notify"), pathItem.Operations);
Assert.DoesNotContain(new HttpMethod("subscribe"), pathItem.Operations);

var additionalPathsExt = Assert.IsType<JsonNodeExtension>(Assert.Contains("x-oai-additionalOperations", pathItem.Extensions));

var additionalOpsNode = Assert.IsType<JsonObject>(additionalPathsExt.Node);
Assert.Contains("query", additionalOpsNode);
Assert.Contains("notify", additionalOpsNode);
Assert.Contains("subscribe", additionalOpsNode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
openapi: 3.1.0
info:
title: PathItem with Query and AdditionalOperations
version: 1.0.0
paths:
/pets:
summary: Pet operations
description: Operations available for pets
get:
summary: Get pets
operationId: getPets
responses:
'200':
description: List of pets
x-oai-additionalOperations:
query:
summary: Query pets with complex filters
operationId: queryPets
parameters:
- name: filter
in: query
description: Complex filter expression
schema:
type: string
responses:
'200':
description: Filtered pets
content:
application/json:
schema:
type: array
items:
type: object
notify:
summary: Notify about pet updates
operationId: notifyPetUpdates
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
petId:
type: string
event:
type: string
responses:
'200':
description: Notification sent
subscribe:
summary: Subscribe to pet events
operationId: subscribePetEvents
parameters:
- name: events
in: query
description: Event types to subscribe to
schema:
type: array
items:
type: string
responses:
'200':
description: Subscription created
content:
application/json:
schema:
type: object
properties:
subscriptionId:
type: string
post:
summary: Create pet
operationId: createPet
responses:
'201':
description: Pet created
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.OpenApi.Readers.Tests.V32Tests;

public class OpenApiPathItemDeserializerTests
{
private const string SampleFolderPath = "V32Tests/Samples/OpenApiPathItem/";

[Fact]
public async Task ParsePathItemWithQueryAndAdditionalOperationsV32Works()
{
// Arrange & Act
var result = await OpenApiDocument.LoadAsync($"{SampleFolderPath}pathItemWithQueryAndAdditionalOperations.yaml", SettingsFixture.ReaderSettings);
var pathItem = result.Document.Paths["/pets"];

// Assert
Assert.Equal("Pet operations", pathItem.Summary);
Assert.Equal("Operations available for pets", pathItem.Description);

// Regular operations
var getOp = Assert.Contains(HttpMethod.Get, pathItem.Operations);
Assert.Equal("getPets", getOp.OperationId);

var postOp = Assert.Contains(HttpMethod.Post, pathItem.Operations);
Assert.Equal("createPet", postOp.OperationId);

// Query operation should now be on one of the operations
// Since the YAML structure changed, we need to check which operation has the query
var queryOp = Assert.Contains(new HttpMethod("Query"), pathItem.Operations);
Assert.Equal("Query pets with complex filters", queryOp.Summary);
Assert.Equal("queryPets", queryOp.OperationId);
Assert.Single(queryOp.Parameters);
Assert.Equal("filter", queryOp.Parameters[0].Name);

var notifyOp = Assert.Contains(new HttpMethod("notify"), pathItem.Operations);
Assert.Equal("Notify about pet updates", notifyOp.Summary);
Assert.Equal("notifyPetUpdates", notifyOp.OperationId);
Assert.NotNull(notifyOp.RequestBody);

var subscribeOp = Assert.Contains(new HttpMethod("subscribe"), pathItem.Operations);
Assert.Equal("Subscribe to pet events", subscribeOp.Summary);
Assert.Equal("subscribePetEvents", subscribeOp.OperationId);
Assert.Single(subscribeOp.Parameters);
Assert.Equal("events", subscribeOp.Parameters[0].Name);
}
}
Loading