Skip to content

Commit 13e3f99

Browse files
Update walkapi to support 3.2 additionalOperations
1 parent bbb8c3a commit 13e3f99

File tree

3 files changed

+180
-0
lines changed

3 files changed

+180
-0
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
openapi: 3.2.0
2+
info:
3+
title: AdditionalOperations Test API
4+
version: 1.0.0
5+
description: API for testing additionalOperations walk functionality
6+
7+
paths:
8+
/custom/{id}:
9+
summary: Custom operations path
10+
description: Path with custom HTTP methods in additionalOperations
11+
parameters:
12+
- name: id
13+
in: path
14+
description: Resource ID
15+
required: true
16+
schema:
17+
type: string
18+
get:
19+
operationId: getCustomResource
20+
summary: Get custom resource
21+
description: Standard GET operation
22+
responses:
23+
"200":
24+
description: Success
25+
content:
26+
application/json:
27+
schema:
28+
type: object
29+
properties:
30+
id:
31+
type: string
32+
name:
33+
type: string
34+
additionalOperations:
35+
COPY:
36+
operationId: copyCustomResource
37+
summary: Copy custom resource
38+
description: Custom COPY operation to duplicate a resource
39+
tags:
40+
- custom
41+
parameters:
42+
- name: destination
43+
in: header
44+
description: Destination for copy operation
45+
required: true
46+
schema:
47+
type: string
48+
responses:
49+
"201":
50+
description: Resource copied successfully
51+
content:
52+
application/json:
53+
schema:
54+
type: object
55+
properties:
56+
id:
57+
type: string
58+
originalId:
59+
type: string
60+
message:
61+
type: string
62+
"400":
63+
description: Invalid copy request
64+
x-custom: copy-operation-extension
65+
PURGE:
66+
operationId: purgeCustomResource
67+
summary: Purge custom resource
68+
description: Custom PURGE operation to completely remove a resource
69+
tags:
70+
- custom
71+
responses:
72+
"204":
73+
description: Resource purged successfully
74+
"404":
75+
description: Resource not found
76+
x-custom: purge-operation-extension
77+
x-custom: custom-path-item-extension
78+
79+
/standard:
80+
get:
81+
operationId: getStandardResource
82+
summary: Get standard resource
83+
description: Standard operation without additionalOperations
84+
responses:
85+
"200":
86+
description: Success
87+
88+
components:
89+
schemas:
90+
CustomResource:
91+
type: object
92+
description: Custom resource object
93+
properties:
94+
id:
95+
type: string
96+
description: Resource identifier
97+
name:
98+
type: string
99+
description: Resource name
100+
data:
101+
type: object
102+
description: Resource data
103+
required:
104+
- id
105+
- name
106+
107+
x-custom: root-extension

openapi/walk.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,15 @@ func walkPathItem(ctx context.Context, pathItem *PathItem, parent MatchFunc, loc
275275
}
276276
}
277277

278+
// Walk through additional operations (OpenAPI 3.2+)
279+
if pathItem.AdditionalOperations != nil {
280+
for method, operation := range pathItem.AdditionalOperations.All() {
281+
if !walkOperation(ctx, operation, append(loc, LocationContext{ParentMatchFunc: parent, ParentField: "additionalOperations", ParentKey: pointer.From(method)}), openAPI, yield) {
282+
return false
283+
}
284+
}
285+
}
286+
278287
// Visit PathItem Extensions
279288
return yield(WalkItem{Match: getMatchFunc(pathItem.Extensions), Location: append(loc, LocationContext{ParentMatchFunc: parent, ParentField: ""}), OpenAPI: openAPI})
280289
}

openapi/walk_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,3 +1084,67 @@ func TestWalk_Terminate_Success(t *testing.T) {
10841084

10851085
assert.Equal(t, 1, visits, "expected only one visit before terminating")
10861086
}
1087+
1088+
func TestWalkAdditionalOperations_Success(t *testing.T) {
1089+
t.Parallel()
1090+
1091+
// Load OpenAPI document with additionalOperations
1092+
f, err := os.Open("testdata/walk.additionaloperations.openapi.yaml")
1093+
require.NoError(t, err)
1094+
defer f.Close()
1095+
1096+
openAPIDoc, validationErrs, err := openapi.Unmarshal(t.Context(), f)
1097+
require.NoError(t, err)
1098+
require.Empty(t, validationErrs, "Document should be valid")
1099+
1100+
matchedLocations := []string{}
1101+
expectedAssertions := map[string]func(*openapi.Operation){
1102+
"/paths/~1custom~1{id}/get": func(op *openapi.Operation) {
1103+
assert.Equal(t, "getCustomResource", op.GetOperationID())
1104+
assert.Equal(t, "Get custom resource", op.GetSummary())
1105+
},
1106+
"/paths/~1custom~1{id}/additionalOperations/COPY": func(op *openapi.Operation) {
1107+
assert.Equal(t, "copyCustomResource", op.GetOperationID())
1108+
assert.Equal(t, "Copy custom resource", op.GetSummary())
1109+
assert.Equal(t, "Custom COPY operation to duplicate a resource", op.GetDescription())
1110+
assert.Contains(t, op.GetTags(), "custom")
1111+
},
1112+
"/paths/~1custom~1{id}/additionalOperations/PURGE": func(op *openapi.Operation) {
1113+
assert.Equal(t, "purgeCustomResource", op.GetOperationID())
1114+
assert.Equal(t, "Purge custom resource", op.GetSummary())
1115+
assert.Equal(t, "Custom PURGE operation to completely remove a resource", op.GetDescription())
1116+
assert.Contains(t, op.GetTags(), "custom")
1117+
},
1118+
"/paths/~1standard/get": func(op *openapi.Operation) {
1119+
assert.Equal(t, "getStandardResource", op.GetOperationID())
1120+
assert.Equal(t, "Get standard resource", op.GetSummary())
1121+
},
1122+
}
1123+
1124+
for item := range openapi.Walk(t.Context(), openAPIDoc) {
1125+
err := item.Match(openapi.Matcher{
1126+
Operation: func(op *openapi.Operation) error {
1127+
operationLoc := string(item.Location.ToJSONPointer())
1128+
matchedLocations = append(matchedLocations, operationLoc)
1129+
1130+
if assertFunc, exists := expectedAssertions[operationLoc]; exists {
1131+
assertFunc(op)
1132+
}
1133+
1134+
return nil
1135+
},
1136+
})
1137+
require.NoError(t, err)
1138+
}
1139+
1140+
// Verify all expected operations were visited
1141+
for expectedLoc := range expectedAssertions {
1142+
assert.Contains(t, matchedLocations, expectedLoc, "Should visit operation at location: %s", expectedLoc)
1143+
}
1144+
1145+
// Verify we found both standard and additional operations
1146+
assert.Contains(t, matchedLocations, "/paths/~1custom~1{id}/get", "Should visit standard GET operation")
1147+
assert.Contains(t, matchedLocations, "/paths/~1custom~1{id}/additionalOperations/COPY", "Should visit additional COPY operation")
1148+
assert.Contains(t, matchedLocations, "/paths/~1custom~1{id}/additionalOperations/PURGE", "Should visit additional PURGE operation")
1149+
assert.Contains(t, matchedLocations, "/paths/~1standard/get", "Should visit standard operation on path without additionalOperations")
1150+
}

0 commit comments

Comments
 (0)