Skip to content

Commit fd2a04c

Browse files
Added upgrade path for 3.2 additionalOperations
1 parent b79f07f commit fd2a04c

File tree

4 files changed

+235
-2
lines changed

4 files changed

+235
-2
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
openapi: 3.1.0
2+
info:
3+
title: Test API with Custom Methods
4+
version: 1.0.0
5+
description: Test document with non-standard HTTP methods
6+
paths:
7+
/test:
8+
get:
9+
summary: Standard GET operation
10+
responses:
11+
"200":
12+
description: OK
13+
content:
14+
application/json:
15+
schema:
16+
type: object
17+
properties:
18+
message:
19+
type: string
20+
copy:
21+
summary: Custom COPY operation
22+
responses:
23+
"200":
24+
description: Resource copied
25+
content:
26+
application/json:
27+
schema:
28+
type: object
29+
properties:
30+
success:
31+
type: boolean
32+
move:
33+
summary: Custom MOVE operation
34+
responses:
35+
"200":
36+
description: Resource moved
37+
content:
38+
application/json:
39+
schema:
40+
type: object
41+
properties:
42+
newLocation:
43+
type: string
44+
/another:
45+
post:
46+
summary: Standard POST operation
47+
responses:
48+
"201":
49+
description: Created
50+
purge:
51+
summary: Custom PURGE operation
52+
responses:
53+
"204":
54+
description: Purged
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
openapi: 3.2.0
2+
info:
3+
title: Test API with Custom Methods
4+
version: 1.0.0
5+
description: Test document with non-standard HTTP methods
6+
paths:
7+
/test:
8+
get:
9+
summary: Standard GET operation
10+
responses:
11+
"200":
12+
description: OK
13+
content:
14+
application/json:
15+
schema:
16+
type: object
17+
properties:
18+
message:
19+
type: string
20+
additionalOperations:
21+
copy:
22+
summary: Custom COPY operation
23+
responses:
24+
"200":
25+
description: Resource copied
26+
content:
27+
application/json:
28+
schema:
29+
type: object
30+
properties:
31+
success:
32+
type: boolean
33+
move:
34+
summary: Custom MOVE operation
35+
responses:
36+
"200":
37+
description: Resource moved
38+
content:
39+
application/json:
40+
schema:
41+
type: object
42+
properties:
43+
newLocation:
44+
type: string
45+
/another:
46+
post:
47+
summary: Standard POST operation
48+
responses:
49+
"201":
50+
description: Created
51+
additionalOperations:
52+
purge:
53+
summary: Custom PURGE operation
54+
responses:
55+
"204":
56+
description: Purged

openapi/upgrade.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/speakeasy-api/openapi/internal/version"
99
"github.com/speakeasy-api/openapi/jsonschema/oas3"
1010
"github.com/speakeasy-api/openapi/marshaller"
11+
"github.com/speakeasy-api/openapi/sequencedmap"
1112
"gopkg.in/yaml.v3"
1213
)
1314

@@ -112,12 +113,14 @@ func upgradeFrom310To312(_ context.Context, doc *OpenAPI, currentVersion *versio
112113
doc.OpenAPI = maxVersion.String()
113114
}
114115

115-
func upgradeFrom31To32(_ context.Context, doc *OpenAPI, currentVersion *version.Version, targetVersion *version.Version) {
116+
func upgradeFrom31To32(ctx context.Context, doc *OpenAPI, currentVersion *version.Version, targetVersion *version.Version) {
116117
if !targetVersion.GreaterThan(*currentVersion) {
117118
return
118119
}
119120

120-
// TODO: Upgrade path additionalOperations for non-standard HTTP methods
121+
// Upgrade path additionalOperations for non-standard HTTP methods
122+
migrateAdditionalOperations31to32(ctx, doc)
123+
121124
// TODO: Upgrade tags such as x-displayName to summary, and x-tagGroups with parents, etc.
122125

123126
// Currently no breaking changes between 3.1.x and 3.2.x that need to be handled
@@ -131,6 +134,46 @@ func upgradeFrom31To32(_ context.Context, doc *OpenAPI, currentVersion *version.
131134
doc.OpenAPI = maxVersion.String()
132135
}
133136

137+
// migrateAdditionalOperations31to32 migrates non-standard HTTP methods from the main operations map
138+
// to the additionalOperations field in PathItem objects for OpenAPI 3.2.0+ compatibility.
139+
func migrateAdditionalOperations31to32(_ context.Context, doc *OpenAPI) {
140+
if doc.Paths == nil {
141+
return
142+
}
143+
144+
for _, referencedPathItem := range doc.Paths.All() {
145+
if referencedPathItem == nil || referencedPathItem.Object == nil {
146+
continue
147+
}
148+
149+
pathItem := referencedPathItem.Object
150+
nonStandardMethods := sequencedmap.New[string, *Operation]()
151+
152+
// Find non-standard HTTP methods in the main operations map
153+
for method, operation := range pathItem.All() {
154+
if !IsStandardMethod(string(method)) {
155+
nonStandardMethods.Set(string(method), operation)
156+
}
157+
}
158+
159+
// If we found non-standard methods, migrate them to additionalOperations
160+
if nonStandardMethods.Len() > 0 {
161+
// Initialize additionalOperations if it doesn't exist
162+
if pathItem.AdditionalOperations == nil {
163+
pathItem.AdditionalOperations = sequencedmap.New[string, *Operation]()
164+
}
165+
166+
// Move each non-standard operation to additionalOperations
167+
for method, operation := range nonStandardMethods.All() {
168+
pathItem.AdditionalOperations.Set(method, operation)
169+
170+
// Remove from the main operations map
171+
pathItem.Map.Delete(HTTPMethod(method))
172+
}
173+
}
174+
}
175+
}
176+
134177
func upgradeSchema30to31(js *oas3.JSONSchema[oas3.Referenceable]) {
135178
if js == nil || js.IsReference() || js.IsRight() {
136179
return

openapi/upgrade_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ func TestUpgrade_Success(t *testing.T) {
5757
options: nil,
5858
description: "nullable schema should upgrade to oneOf without panic",
5959
},
60+
{
61+
name: "upgrade_3_1_0_with_custom_methods",
62+
inputFile: "testdata/upgrade/3_1_0_with_custom_methods.yaml",
63+
expectedFile: "testdata/upgrade/expected_3_1_0_with_custom_methods_upgraded.yaml",
64+
options: []openapi.Option[openapi.UpgradeOptions]{openapi.WithUpgradeTargetVersion("3.2.0")},
65+
description: "3.1.0 with custom HTTP methods should migrate to additionalOperations",
66+
targetVersion: "3.2.0",
67+
},
6068
}
6169

6270
for _, tt := range tests {
@@ -311,3 +319,75 @@ components:
311319
assert.Nil(t, simpleExample.Example, "example should be nil")
312320
assert.NotEmpty(t, simpleExample.Examples, "examples should not be empty")
313321
}
322+
323+
func TestUpgradeAdditionalOperations(t *testing.T) {
324+
t.Parallel()
325+
326+
ctx := t.Context()
327+
328+
// Create a document with non-standard HTTP methods
329+
doc := &openapi.OpenAPI{
330+
OpenAPI: "3.1.0",
331+
Info: openapi.Info{
332+
Title: "Test API",
333+
Version: "1.0.0",
334+
},
335+
Paths: openapi.NewPaths(),
336+
}
337+
338+
// Add a path with both standard and non-standard methods
339+
pathItem := openapi.NewPathItem()
340+
341+
// Standard method
342+
pathItem.Set(openapi.HTTPMethodGet, &openapi.Operation{
343+
Summary: &[]string{"Get operation"}[0],
344+
Responses: openapi.NewResponses(),
345+
})
346+
347+
// Non-standard methods
348+
pathItem.Set(openapi.HTTPMethod("copy"), &openapi.Operation{
349+
Summary: &[]string{"Copy operation"}[0],
350+
Responses: openapi.NewResponses(),
351+
})
352+
353+
pathItem.Set(openapi.HTTPMethod("purge"), &openapi.Operation{
354+
Summary: &[]string{"Purge operation"}[0],
355+
Responses: openapi.NewResponses(),
356+
})
357+
358+
doc.Paths.Set("/test", &openapi.ReferencedPathItem{Object: pathItem})
359+
360+
// Verify initial state
361+
assert.Equal(t, 3, pathItem.Len(), "should have 3 operations initially")
362+
assert.Nil(t, pathItem.AdditionalOperations, "additionalOperations should be nil initially")
363+
assert.NotNil(t, pathItem.GetOperation(openapi.HTTPMethod("copy")), "copy operation should exist in main map")
364+
assert.NotNil(t, pathItem.GetOperation(openapi.HTTPMethod("purge")), "purge operation should exist in main map")
365+
366+
// Perform upgrade to 3.2.0
367+
upgraded, err := openapi.Upgrade(ctx, doc, openapi.WithUpgradeTargetVersion("3.2.0"))
368+
require.NoError(t, err, "upgrade should not fail")
369+
assert.True(t, upgraded, "upgrade should have been performed")
370+
assert.Equal(t, "3.2.0", doc.OpenAPI, "version should be 3.2.0")
371+
372+
// Verify migration results
373+
assert.Equal(t, 1, pathItem.Len(), "should have only 1 operation in main map after migration")
374+
assert.NotNil(t, pathItem.AdditionalOperations, "additionalOperations should be initialized")
375+
assert.Equal(t, 2, pathItem.AdditionalOperations.Len(), "should have 2 operations in additionalOperations")
376+
377+
// Verify standard method remains in main map
378+
assert.NotNil(t, pathItem.GetOperation(openapi.HTTPMethodGet), "get operation should remain in main map")
379+
380+
// Verify non-standard methods are moved to additionalOperations
381+
assert.Nil(t, pathItem.GetOperation(openapi.HTTPMethod("copy")), "copy operation should be removed from main map")
382+
assert.Nil(t, pathItem.GetOperation(openapi.HTTPMethod("purge")), "purge operation should be removed from main map")
383+
384+
copyOp, exists := pathItem.AdditionalOperations.Get("copy")
385+
assert.True(t, exists, "copy operation should exist in additionalOperations")
386+
assert.NotNil(t, copyOp, "copy operation should not be nil")
387+
assert.Equal(t, "Copy operation", *copyOp.Summary, "copy operation summary should be preserved")
388+
389+
purgeOp, exists := pathItem.AdditionalOperations.Get("purge")
390+
assert.True(t, exists, "purge operation should exist in additionalOperations")
391+
assert.NotNil(t, purgeOp, "purge operation should not be nil")
392+
assert.Equal(t, "Purge operation", *purgeOp.Summary, "purge operation summary should be preserved")
393+
}

0 commit comments

Comments
 (0)