Skip to content

Commit a27a0ad

Browse files
feat: add --version flag to upgrade command (#160)
1 parent 0ae87b9 commit a27a0ad

File tree

2 files changed

+197
-13
lines changed

2 files changed

+197
-13
lines changed

cmd/openapi/commands/openapi/upgrade.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ With --minor-only, only performs cross-minor version upgrades:
2929
- 3.1.x → 3.2.0 (cross-minor upgrade)
3030
- 3.2.x → no change (same minor version, skip patch upgrades)
3131
32+
With --version, upgrades to a specific OpenAPI version instead of the latest:
33+
- openapi spec upgrade --version 3.1.0 spec.yaml
34+
(upgrades a 3.0.x spec to 3.1.0)
35+
36+
Note: --version and --minor-only are mutually exclusive.
37+
3238
The upgrade process includes:
3339
- Updating the OpenAPI version field
3440
- Converting nullable properties to proper JSON Schema format
@@ -44,19 +50,26 @@ Output options:
4450
}
4551

4652
var (
47-
minorOnly bool
48-
writeInPlace bool
53+
minorOnly bool
54+
writeInPlace bool
55+
targetVersion string
4956
)
5057

5158
func init() {
5259
upgradeCmd.Flags().BoolVar(&minorOnly, "minor-only", false, "only upgrade across minor versions, skip patch-level upgrades within same minor")
5360
upgradeCmd.Flags().BoolVarP(&writeInPlace, "write", "w", false, "write result in-place to input file")
61+
upgradeCmd.Flags().StringVarP(&targetVersion, "version", "V", "", "target OpenAPI version to upgrade to (default latest)")
5462
}
5563

5664
func runUpgrade(cmd *cobra.Command, args []string) {
5765
ctx := cmd.Context()
5866
inputFile := inputFileFromArgs(args)
5967

68+
if targetVersion != "" && minorOnly {
69+
fmt.Fprintf(os.Stderr, "Error: --version and --minor-only are mutually exclusive\n")
70+
os.Exit(1)
71+
}
72+
6073
outputFile := outputFileFromArgs(args)
6174

6275
processor, err := NewOpenAPIProcessor(inputFile, outputFile, writeInPlace)
@@ -65,13 +78,27 @@ func runUpgrade(cmd *cobra.Command, args []string) {
6578
os.Exit(1)
6679
}
6780

68-
if err := upgradeOpenAPI(ctx, processor, !minorOnly); err != nil {
81+
var opts []openapi.Option[openapi.UpgradeOptions]
82+
if targetVersion != "" {
83+
opts = append(opts, openapi.WithUpgradeTargetVersion(targetVersion))
84+
// Enable same-minor upgrades so patch-level targets work as expected.
85+
// Without this, --version 3.1.2 on a 3.1.0 doc would be silently
86+
// skipped because they share the same minor version.
87+
opts = append(opts, openapi.WithUpgradeSameMinorVersion())
88+
} else if !minorOnly {
89+
// By default, upgrade all versions including patch upgrades (e.g., 3.2.0 → 3.2.1)
90+
opts = append(opts, openapi.WithUpgradeSameMinorVersion())
91+
}
92+
// When minorOnly is true, only cross-minor upgrades are performed
93+
// Patch upgrades within the same minor version (e.g., 3.2.0 → 3.2.1) are skipped
94+
95+
if err := upgradeOpenAPI(ctx, processor, opts...); err != nil {
6996
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
7097
os.Exit(1)
7198
}
7299
}
73100

74-
func upgradeOpenAPI(ctx context.Context, processor *OpenAPIProcessor, upgradeSameMinorVersion bool) error {
101+
func upgradeOpenAPI(ctx context.Context, processor *OpenAPIProcessor, opts ...openapi.Option[openapi.UpgradeOptions]) error {
75102
// Load the OpenAPI document
76103
doc, validationErrors, err := processor.LoadDocument(ctx)
77104
if err != nil {
@@ -84,15 +111,6 @@ func upgradeOpenAPI(ctx context.Context, processor *OpenAPIProcessor, upgradeSam
84111
// Report validation errors but continue with upgrade
85112
processor.ReportValidationErrors(validationErrors)
86113

87-
// Prepare upgrade options
88-
var opts []openapi.Option[openapi.UpgradeOptions]
89-
if upgradeSameMinorVersion {
90-
// By default, upgrade all versions including patch upgrades (e.g., 3.2.0 → 3.2.1)
91-
opts = append(opts, openapi.WithUpgradeSameMinorVersion())
92-
}
93-
// When minorOnly is true, only cross-minor upgrades are performed
94-
// Patch upgrades within the same minor version (e.g., 3.2.0 → 3.2.1) are skipped
95-
96114
// Perform the upgrade
97115
originalVersion := doc.OpenAPI
98116
upgraded, err := openapi.Upgrade(ctx, doc, opts...)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package openapi
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
"github.com/speakeasy-api/openapi/openapi"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestUpgradeOpenAPI_ValidVersionTransition(t *testing.T) {
14+
t.Parallel()
15+
16+
tests := []struct {
17+
name string
18+
inputDoc string
19+
opts []openapi.Option[openapi.UpgradeOptions]
20+
expectedVersion string
21+
expectUpgraded bool
22+
}{
23+
{
24+
name: "upgrades 3.0.3 to latest by default",
25+
inputDoc: `openapi: "3.0.3"
26+
info:
27+
title: Test
28+
version: "1.0"
29+
paths: {}
30+
`,
31+
opts: []openapi.Option[openapi.UpgradeOptions]{openapi.WithUpgradeSameMinorVersion()},
32+
expectedVersion: openapi.Version,
33+
expectUpgraded: true,
34+
},
35+
{
36+
name: "upgrades 3.1.0 to latest by default",
37+
inputDoc: `openapi: "3.1.0"
38+
info:
39+
title: Test
40+
version: "1.0"
41+
paths: {}
42+
`,
43+
opts: []openapi.Option[openapi.UpgradeOptions]{openapi.WithUpgradeSameMinorVersion()},
44+
expectedVersion: openapi.Version,
45+
expectUpgraded: true,
46+
},
47+
{
48+
name: "upgrades 3.0.3 to target version 3.1.0",
49+
inputDoc: `openapi: "3.0.3"
50+
info:
51+
title: Test
52+
version: "1.0"
53+
paths: {}
54+
`,
55+
opts: []openapi.Option[openapi.UpgradeOptions]{
56+
openapi.WithUpgradeTargetVersion("3.1.0"),
57+
openapi.WithUpgradeSameMinorVersion(),
58+
},
59+
expectedVersion: "3.1.0",
60+
expectUpgraded: true,
61+
},
62+
{
63+
name: "no upgrade needed when already at target version",
64+
inputDoc: `openapi: "3.1.0"
65+
info:
66+
title: Test
67+
version: "1.0"
68+
paths: {}
69+
`,
70+
opts: []openapi.Option[openapi.UpgradeOptions]{
71+
openapi.WithUpgradeTargetVersion("3.1.0"),
72+
openapi.WithUpgradeSameMinorVersion(),
73+
},
74+
expectedVersion: "3.1.0",
75+
expectUpgraded: false,
76+
},
77+
{
78+
name: "minor-only skips same-minor upgrade",
79+
inputDoc: `openapi: "3.2.0"
80+
info:
81+
title: Test
82+
version: "1.0"
83+
paths: {}
84+
`,
85+
opts: nil, // no WithUpgradeSameMinorVersion
86+
expectedVersion: "3.2.0",
87+
expectUpgraded: false,
88+
},
89+
}
90+
91+
for _, tt := range tests {
92+
t.Run(tt.name, func(t *testing.T) {
93+
t.Parallel()
94+
95+
var stdout, stderr bytes.Buffer
96+
processor := &OpenAPIProcessor{
97+
InputFile: "-",
98+
ReadFromStdin: true,
99+
WriteToStdout: true,
100+
Stdin: strings.NewReader(tt.inputDoc),
101+
Stdout: &stdout,
102+
Stderr: &stderr,
103+
}
104+
105+
err := upgradeOpenAPI(t.Context(), processor, tt.opts...)
106+
require.NoError(t, err, "upgradeOpenAPI should succeed")
107+
108+
assert.Contains(t, stdout.String(), "openapi: \""+tt.expectedVersion+"\"",
109+
"output should contain the expected version")
110+
111+
if tt.expectUpgraded {
112+
assert.Contains(t, stderr.String(), "Successfully upgraded",
113+
"stderr should report successful upgrade")
114+
} else {
115+
assert.Contains(t, stderr.String(), "No upgrade needed",
116+
"stderr should report no upgrade needed")
117+
}
118+
})
119+
}
120+
}
121+
122+
func TestUpgradeOpenAPI_InvalidVersionTransition(t *testing.T) {
123+
t.Parallel()
124+
125+
tests := []struct {
126+
name string
127+
inputDoc string
128+
opts []openapi.Option[openapi.UpgradeOptions]
129+
expectedErr string
130+
}{
131+
{
132+
name: "cannot downgrade version",
133+
inputDoc: `openapi: "3.2.0"
134+
info:
135+
title: Test
136+
version: "1.0"
137+
paths: {}
138+
`,
139+
opts: []openapi.Option[openapi.UpgradeOptions]{
140+
openapi.WithUpgradeTargetVersion("3.1.0"),
141+
openapi.WithUpgradeSameMinorVersion(),
142+
},
143+
expectedErr: "cannot downgrade",
144+
},
145+
}
146+
147+
for _, tt := range tests {
148+
t.Run(tt.name, func(t *testing.T) {
149+
t.Parallel()
150+
151+
var stdout, stderr bytes.Buffer
152+
processor := &OpenAPIProcessor{
153+
InputFile: "-",
154+
ReadFromStdin: true,
155+
WriteToStdout: true,
156+
Stdin: strings.NewReader(tt.inputDoc),
157+
Stdout: &stdout,
158+
Stderr: &stderr,
159+
}
160+
161+
err := upgradeOpenAPI(t.Context(), processor, tt.opts...)
162+
require.Error(t, err, "upgradeOpenAPI should return an error")
163+
assert.Contains(t, err.Error(), tt.expectedErr, "error should contain expected message")
164+
})
165+
}
166+
}

0 commit comments

Comments
 (0)