Skip to content

Commit ee2f0a2

Browse files
feat: add support for 3.2.0 openapi spec
1 parent 6c51416 commit ee2f0a2

34 files changed

+656
-296
lines changed

arazzo/arazzo.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/speakeasy-api/openapi/arazzo/core"
1212
"github.com/speakeasy-api/openapi/extensions"
1313
"github.com/speakeasy-api/openapi/internal/interfaces"
14-
"github.com/speakeasy-api/openapi/internal/utils"
14+
"github.com/speakeasy-api/openapi/internal/version"
1515
"github.com/speakeasy-api/openapi/jsonschema/oas3"
1616
"github.com/speakeasy-api/openapi/marshaller"
1717
"github.com/speakeasy-api/openapi/pointer"
@@ -20,12 +20,31 @@ import (
2020

2121
// Version is the version of the Arazzo Specification that this package conforms to.
2222
const (
23-
Version = "1.0.1"
24-
VersionMajor = 1
25-
VersionMinor = 0
26-
VersionPatch = 1
23+
Version = "1.0.1"
2724
)
2825

26+
func MinimumSupportedVersion() version.Version {
27+
v, err := version.ParseVersion("1.0.0")
28+
if err != nil {
29+
panic("failed to parse minimum supported Arazzo version: " + err.Error())
30+
}
31+
if v == nil {
32+
panic("minimum supported Arazzo version is nil")
33+
}
34+
return *v
35+
}
36+
37+
func MaximumSupportedVersion() version.Version {
38+
v, err := version.ParseVersion(Version)
39+
if err != nil {
40+
panic("failed to parse maximum supported Arazzo version: " + err.Error())
41+
}
42+
if v == nil {
43+
panic("maximum supported Arazzo version is nil")
44+
}
45+
return *v
46+
}
47+
2948
// Arazzo is the root object for an Arazzo document.
3049
type Arazzo struct {
3150
marshaller.Model[core.Arazzo]
@@ -105,13 +124,14 @@ func (a *Arazzo) Validate(ctx context.Context, opts ...validation.Option) []erro
105124
core := a.GetCore()
106125
errs := []error{}
107126

108-
arazzoMajor, arazzoMinor, arazzoPatch, err := utils.ParseVersion(a.Arazzo)
127+
arazzoVersion, err := version.ParseVersion(a.Arazzo)
109128
if err != nil {
110129
errs = append(errs, validation.NewValueError(validation.NewValueValidationError("arazzo field version is invalid %s: %s", a.Arazzo, err.Error()), core, core.Arazzo))
111130
}
112-
113-
if arazzoMajor != VersionMajor || arazzoMinor != VersionMinor || arazzoPatch > VersionPatch {
114-
errs = append(errs, validation.NewValueError(validation.NewValueValidationError("arazzo field version only %s and below is supported", Version), core, core.Arazzo))
131+
if arazzoVersion != nil {
132+
if arazzoVersion.GreaterThan(MaximumSupportedVersion()) {
133+
errs = append(errs, validation.NewValueError(validation.NewValueValidationError("arazzo field version only Arazzo versions between %s and %s are supported", MinimumSupportedVersion(), MaximumSupportedVersion()), core, core.Arazzo))
134+
}
115135
}
116136

117137
errs = append(errs, a.Info.Validate(ctx, opts...)...)

arazzo/arazzo_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ sourceDescriptions:
304304
underlyingError error
305305
}{
306306
{line: 1, column: 1, underlyingError: validation.NewMissingFieldError("arazzo field workflows is missing")},
307-
{line: 1, column: 9, underlyingError: validation.NewValueValidationError("arazzo field version only 1.0.1 and below is supported")},
307+
{line: 1, column: 9, underlyingError: validation.NewValueValidationError("arazzo field version only Arazzo versions between 1.0.0 and 1.0.1 are supported")},
308308
{line: 4, column: 3, underlyingError: validation.NewMissingFieldError("info field version is missing")},
309309
{line: 6, column: 5, underlyingError: validation.NewMissingFieldError("sourceDescription field url is missing")},
310310
{line: 7, column: 11, underlyingError: validation.NewValueValidationError("sourceDescription field type must be one of [openapi, arazzo]")},

internal/utils/versions.go

Lines changed: 0 additions & 40 deletions
This file was deleted.

internal/version/version.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package version
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
type Version struct {
10+
Major int
11+
Minor int
12+
Patch int
13+
}
14+
15+
func New(major, minor, patch int) *Version {
16+
return &Version{
17+
Major: major,
18+
Minor: minor,
19+
Patch: patch,
20+
}
21+
}
22+
23+
func (v Version) String() string {
24+
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
25+
}
26+
27+
func (v Version) Equal(other Version) bool {
28+
return v.Major == other.Major && v.Minor == other.Minor && v.Patch == other.Patch
29+
}
30+
31+
func (v Version) GreaterThan(other Version) bool {
32+
if v.Major > other.Major {
33+
return true
34+
} else if v.Major < other.Major {
35+
return false
36+
}
37+
38+
if v.Minor > other.Minor {
39+
return true
40+
} else if v.Minor < other.Minor {
41+
return false
42+
}
43+
44+
return v.Patch > other.Patch
45+
}
46+
47+
func (v Version) LessThan(other Version) bool {
48+
return !v.Equal(other) && !v.GreaterThan(other)
49+
}
50+
51+
func ParseVersion(version string) (*Version, error) {
52+
parts := strings.Split(version, ".")
53+
if len(parts) != 3 {
54+
return nil, fmt.Errorf("invalid version %s", version)
55+
}
56+
57+
major, err := strconv.Atoi(parts[0])
58+
if err != nil {
59+
return nil, fmt.Errorf("invalid major version %s: %w", parts[0], err)
60+
}
61+
if major < 0 {
62+
return nil, fmt.Errorf("invalid major version %s: cannot be negative", parts[0])
63+
}
64+
65+
minor, err := strconv.Atoi(parts[1])
66+
if err != nil {
67+
return nil, fmt.Errorf("invalid minor version %s: %w", parts[1], err)
68+
}
69+
if minor < 0 {
70+
return nil, fmt.Errorf("invalid minor version %s: cannot be negative", parts[1])
71+
}
72+
73+
patch, err := strconv.Atoi(parts[2])
74+
if err != nil {
75+
return nil, fmt.Errorf("invalid patch version %s: %w", parts[2], err)
76+
}
77+
if patch < 0 {
78+
return nil, fmt.Errorf("invalid patch version %s: cannot be negative", parts[2])
79+
}
80+
81+
return New(major, minor, patch), nil
82+
}
83+
84+
func IsVersionGreaterOrEqual(a, b string) (bool, error) {
85+
versionA, err := ParseVersion(a)
86+
if err != nil {
87+
return false, fmt.Errorf("invalid version %s: %w", a, err)
88+
}
89+
90+
versionB, err := ParseVersion(b)
91+
if err != nil {
92+
return false, fmt.Errorf("invalid version %s: %w", b, err)
93+
}
94+
return versionA.Equal(*versionB) || versionA.GreaterThan(*versionB), nil
95+
}
96+
97+
func IsVersionLessThan(a, b string) (bool, error) {
98+
greaterOrEqual, err := IsVersionGreaterOrEqual(a, b)
99+
if err != nil {
100+
return false, err
101+
}
102+
return !greaterOrEqual, nil
103+
}

internal/utils/versions_test.go renamed to internal/version/version_test.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package utils
1+
package version
22

33
import (
44
"testing"
@@ -52,11 +52,11 @@ func Test_ParseVersion_Success(t *testing.T) {
5252
for _, tt := range tests {
5353
t.Run(tt.name, func(t *testing.T) {
5454
t.Parallel()
55-
major, minor, patch, err := ParseVersion(tt.args.version)
55+
version, err := ParseVersion(tt.args.version)
5656
require.NoError(t, err)
57-
assert.Equal(t, tt.expectedMajor, major)
58-
assert.Equal(t, tt.expectedMinor, minor)
59-
assert.Equal(t, tt.expectedPatch, patch)
57+
assert.Equal(t, tt.expectedMajor, version.Major)
58+
assert.Equal(t, tt.expectedMinor, version.Minor)
59+
assert.Equal(t, tt.expectedPatch, version.Patch)
6060
})
6161
}
6262
}
@@ -131,11 +131,9 @@ func Test_ParseVersion_Error(t *testing.T) {
131131
for _, tt := range tests {
132132
t.Run(tt.name, func(t *testing.T) {
133133
t.Parallel()
134-
major, minor, patch, err := ParseVersion(tt.args.version)
134+
version, err := ParseVersion(tt.args.version)
135135
require.Error(t, err)
136-
assert.Equal(t, 0, major)
137-
assert.Equal(t, 0, minor)
138-
assert.Equal(t, 0, patch)
136+
assert.Nil(t, version)
139137
})
140138
}
141139
}

openapi/bundle_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func TestBundle_EmptyDocument(t *testing.T) {
103103

104104
// Test with minimal document
105105
doc := &openapi.OpenAPI{
106-
OpenAPI: "3.1.0",
106+
OpenAPI: openapi.Version,
107107
Info: openapi.Info{
108108
Title: "Empty API",
109109
Version: "1.0.0",
@@ -122,7 +122,7 @@ func TestBundle_EmptyDocument(t *testing.T) {
122122
require.NoError(t, err)
123123

124124
// Document should remain unchanged
125-
assert.Equal(t, "3.1.0", doc.OpenAPI)
125+
assert.Equal(t, openapi.Version, doc.OpenAPI)
126126
assert.Equal(t, "Empty API", doc.Info.Title)
127127
assert.Equal(t, "1.0.0", doc.Info.Version)
128128

openapi/callbacks_validate_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/speakeasy-api/openapi/marshaller"
99
"github.com/speakeasy-api/openapi/openapi"
10+
"github.com/speakeasy-api/openapi/validation"
1011
"github.com/stretchr/testify/require"
1112
)
1213

@@ -92,7 +93,7 @@ x-timeout: 30
9293
require.NoError(t, err)
9394
require.Empty(t, validationErrs)
9495

95-
errs := callback.Validate(t.Context())
96+
errs := callback.Validate(t.Context(), validation.WithContextObject(openapi.NewOpenAPI()))
9697
require.Empty(t, errs, "Expected no validation errors")
9798
})
9899
}
@@ -267,7 +268,7 @@ func TestCallback_Validate_Error(t *testing.T) {
267268
var allErrors []error
268269
allErrors = append(allErrors, validationErrs...)
269270

270-
validateErrs := callback.Validate(t.Context())
271+
validateErrs := callback.Validate(t.Context(), validation.WithContextObject(openapi.NewOpenAPI()))
271272
allErrors = append(allErrors, validateErrs...)
272273

273274
require.NotEmpty(t, allErrors, "expected validation errors")
@@ -411,7 +412,7 @@ func TestCallback_Validate_ComplexExpressions(t *testing.T) {
411412
require.NoError(t, err)
412413
require.Empty(t, validationErrs)
413414

414-
errs := callback.Validate(t.Context())
415+
errs := callback.Validate(t.Context(), validation.WithContextObject(openapi.NewOpenAPI()))
415416
require.Empty(t, errs, "Expected no validation errors")
416417
})
417418
}
@@ -508,7 +509,7 @@ x-rate-limit: 100
508509
require.NoError(t, err)
509510
require.Empty(t, validationErrs)
510511

511-
errs := callback.Validate(t.Context())
512+
errs := callback.Validate(t.Context(), validation.WithContextObject(openapi.NewOpenAPI()))
512513
require.Empty(t, errs, "Expected no validation errors")
513514
})
514515
}

openapi/clean_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func TestClean_EmptyDocument_Success(t *testing.T) {
8585

8686
// Test with minimal document (no components)
8787
doc := &openapi.OpenAPI{
88-
OpenAPI: "3.1.0",
88+
OpenAPI: openapi.Version,
8989
Info: openapi.Info{
9090
Title: "Empty API",
9191
Version: "1.0.0",
@@ -103,7 +103,7 @@ func TestClean_NoComponents_Success(t *testing.T) {
103103

104104
// Test with document that has no components section
105105
doc := &openapi.OpenAPI{
106-
OpenAPI: "3.1.0",
106+
OpenAPI: openapi.Version,
107107
Info: openapi.Info{
108108
Title: "API without components",
109109
Version: "1.0.0",

openapi/cmd/upgrade.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ func runUpgrade(cmd *cobra.Command, args []string) {
5959
os.Exit(1)
6060
}
6161

62-
if err := upgradeOpenAPI(ctx, processor, minorOnly); err != nil {
62+
if err := upgradeOpenAPI(ctx, processor, !minorOnly); err != nil {
6363
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
6464
os.Exit(1)
6565
}
6666
}
6767

68-
func upgradeOpenAPI(ctx context.Context, processor *OpenAPIProcessor, minorOnly bool) error {
68+
func upgradeOpenAPI(ctx context.Context, processor *OpenAPIProcessor, upgradeSameMinorVersion bool) error {
6969
// Load the OpenAPI document
7070
doc, validationErrors, err := processor.LoadDocument(ctx)
7171
if err != nil {
@@ -80,11 +80,11 @@ func upgradeOpenAPI(ctx context.Context, processor *OpenAPIProcessor, minorOnly
8080

8181
// Prepare upgrade options
8282
var opts []openapi.Option[openapi.UpgradeOptions]
83-
if !minorOnly {
83+
if upgradeSameMinorVersion {
8484
// By default, upgrade all versions including patch versions (3.1.x to 3.1.1)
85-
opts = append(opts, openapi.WithUpgradeSamePatchVersion())
85+
opts = append(opts, openapi.WithUpgradeSameMinorVersion())
8686
}
87-
// When minorOnly is true, only 3.0.x versions will be upgraded to 3.1.1
87+
// When skipPatchOnly is true, only 3.0.x versions will be upgraded to 3.1.1
8888
// 3.1.x versions will be skipped unless they need minor version upgrade
8989

9090
// Perform the upgrade

openapi/components_validate_test.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,10 @@ securitySchemes:
220220
require.NoError(t, err)
221221
require.Empty(t, validationErrs)
222222

223-
// Create a minimal OpenAPI document for operationId validation
224-
var opts []validation.Option
223+
openAPIDoc := openapi.NewOpenAPI()
225224
if tt.name == "valid_components_with_links" {
226225
// Create OpenAPI document with the required operationId for link validation
227-
openAPIDoc := &openapi.OpenAPI{
228-
Paths: openapi.NewPaths(),
229-
}
226+
openAPIDoc.Paths = openapi.NewPaths()
230227

231228
// Add path with operation that matches the operationId in the test
232229
pathItem := openapi.NewPathItem()
@@ -235,11 +232,9 @@ securitySchemes:
235232
}
236233
pathItem.Set("get", operation)
237234
openAPIDoc.Paths.Set("/users/{username}/repos", &openapi.ReferencedPathItem{Object: pathItem})
238-
239-
opts = append(opts, validation.WithContextObject(openAPIDoc))
240235
}
241236

242-
errs := components.Validate(t.Context(), opts...)
237+
errs := components.Validate(t.Context(), validation.WithContextObject(openAPIDoc))
243238
require.Empty(t, errs, "Expected no validation errors")
244239
})
245240
}

0 commit comments

Comments
 (0)