Skip to content

Commit 42c6f26

Browse files
authored
fix: fix sunset ls to report all sunsetting versions of a resource (#1196)
1 parent 7bf4ab7 commit 42c6f26

2 files changed

Lines changed: 123 additions & 57 deletions

File tree

tools/cli/internal/openapi/sunset/sunset.go

Lines changed: 47 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
package sunset
1616

1717
import (
18-
"maps"
19-
"regexp"
20-
"slices"
2118
"sort"
2219

2320
"github.com/oasdiff/kin-openapi/openapi3"
@@ -45,33 +42,39 @@ func NewListFromSpec(spec *load.SpecInfo) []*Sunset {
4542
for path, pathBody := range paths.Map() {
4643
for operationName, operationBody := range pathBody.Operations() {
4744
teamName := teamName(operationBody)
48-
extensions := successResponseExtensions(operationBody.Responses.Map())
49-
if extensions == nil {
50-
continue
45+
extensionsList := successResponseExtensions(operationBody.Responses.Map())
46+
47+
for _, extensions := range extensionsList {
48+
apiVersion, ok := extensions[apiVersionExtensionName]
49+
if !ok {
50+
continue
51+
}
52+
53+
sunsetExt, ok := extensions[sunsetExtensionName]
54+
if !ok {
55+
continue
56+
}
57+
58+
sunset := Sunset{
59+
Operation: operationName,
60+
Path: path,
61+
SunsetDate: sunsetExt.(string),
62+
Version: apiVersion.(string),
63+
Team: teamName,
64+
}
65+
66+
sunsets = append(sunsets, &sunset)
5167
}
52-
53-
apiVersion, ok := extensions[apiVersionExtensionName]
54-
if !ok {
55-
continue
56-
}
57-
58-
sunsetExt, ok := extensions[sunsetExtensionName]
59-
if !ok {
60-
continue
61-
}
62-
63-
sunset := Sunset{
64-
Operation: operationName,
65-
Path: path,
66-
SunsetDate: sunsetExt.(string),
67-
Version: apiVersion.(string),
68-
Team: teamName,
69-
}
70-
71-
sunsets = append(sunsets, &sunset)
7268
}
7369
}
7470

71+
sort.Slice(sunsets, func(i, j int) bool {
72+
if sunsets[i].SunsetDate != sunsets[j].SunsetDate {
73+
return sunsets[i].SunsetDate < sunsets[j].SunsetDate
74+
}
75+
return sunsets[i].Version < sunsets[j].Version
76+
})
77+
7578
return sunsets
7679
}
7780

@@ -95,7 +98,7 @@ func teamName(op *openapi3.Operation) string {
9598
// Returns:
9699
// - A map of extension names to their values from the first successful response content,
97100
// or nil if no successful responses are found or if none contain relevant extensions
98-
func successResponseExtensions(responsesMap map[string]*openapi3.ResponseRef) map[string]any {
101+
func successResponseExtensions(responsesMap map[string]*openapi3.ResponseRef) []map[string]any {
99102
if val, ok := responsesMap["200"]; ok {
100103
return contentExtensions(val.Value.Content)
101104
}
@@ -112,36 +115,28 @@ func successResponseExtensions(responsesMap map[string]*openapi3.ResponseRef) ma
112115
return nil
113116
}
114117

115-
// contentExtensions extracts extensions from OpenAPI content objects, prioritizing content entries
116-
// with the oldest date in their keys.
118+
// contentExtensions extracts extensions from all OpenAPI content entries that have a sunset extension.
117119
//
118-
// The function sorts content keys by date (in YYYY-MM-DD format) if present, with older dates taking
119-
// precedence. If multiple keys contain dates, it selects the entry with the earliest date.
120+
// The function iterates over all content entries and returns the extensions for each entry
121+
// that contains a sunset extension, allowing multiple API versions with different sunset
122+
// dates to be tracked independently.
120123
//
121124
// Parameters:
122125
// - content: An OpenAPI content map with media types as keys and schema objects as values
123126
//
124127
// Returns:
125-
// - A map of extension names to their values from the selected content entry,
126-
// or nil if the content map is empty or the selected entry has no extensions
127-
//
128-
// Assumption: the older version will have the earliest sunset date.
129-
func contentExtensions(content openapi3.Content) map[string]any {
130-
keysContent := slices.Collect(maps.Keys(content))
131-
// Regex to find a date in YYYY-MM-DD format.
132-
dateRegex := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
133-
// we need the content of the API version with the older date.
134-
sort.Slice(keysContent, func(i, j int) bool {
135-
dateI := dateRegex.FindString(keysContent[i])
136-
dateJ := dateRegex.FindString(keysContent[j])
137-
138-
// If both have dates, compare them as strings.
139-
if dateI != "" && dateJ != "" {
140-
return dateI < dateJ
128+
// - A slice of extension maps, one per content entry that has a sunset extension,
129+
// or nil if no entries have sunset extensions
130+
func contentExtensions(content openapi3.Content) []map[string]any {
131+
var result []map[string]any
132+
for _, mediaType := range content {
133+
if mediaType.Extensions == nil {
134+
continue
141135
}
142-
// Strings with dates should come before those without.
143-
return dateI != ""
144-
})
145-
146-
return content[keysContent[0]].Extensions
136+
if _, ok := mediaType.Extensions[sunsetExtensionName]; !ok {
137+
continue
138+
}
139+
result = append(result, mediaType.Extensions)
140+
}
141+
return result
147142
}

tools/cli/internal/openapi/sunset/sunset_test.go

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,47 @@ func TestNewSunsetListFromSpec(t *testing.T) {
7373
},
7474
expected: nil,
7575
},
76+
{
77+
name: "Multiple versions with sunset extensions",
78+
specInfo: &load.SpecInfo{
79+
Spec: &openapi3.T{
80+
Paths: openapi3.NewPaths(openapi3.WithPath("/example", &openapi3.PathItem{
81+
Get: &openapi3.Operation{
82+
Responses: openapi3.NewResponses(openapi3.WithName("200", &openapi3.Response{
83+
Content: openapi3.Content{
84+
"application/vnd.atlas.2023-01-01+json": &openapi3.MediaType{
85+
Extensions: map[string]any{
86+
sunsetExtensionName: "2025-12-31",
87+
apiVersionExtensionName: "2023-01-01",
88+
},
89+
},
90+
"application/vnd.atlas.2024-05-01+json": &openapi3.MediaType{
91+
Extensions: map[string]any{
92+
sunsetExtensionName: "2025-06-01",
93+
apiVersionExtensionName: "2024-05-01",
94+
},
95+
},
96+
},
97+
})),
98+
},
99+
})),
100+
},
101+
},
102+
expected: []*Sunset{
103+
{
104+
Operation: "GET",
105+
Path: "/example",
106+
Version: "2023-01-01",
107+
SunsetDate: "2025-12-31",
108+
},
109+
{
110+
Operation: "GET",
111+
Path: "/example",
112+
Version: "2024-05-01",
113+
SunsetDate: "2025-06-01",
114+
},
115+
},
116+
},
76117
{
77118
name: "201 operations with extensions",
78119
specInfo: &load.SpecInfo{
@@ -109,7 +150,7 @@ func TestNewSunsetListFromSpec(t *testing.T) {
109150
for _, test := range tests {
110151
t.Run(test.name, func(t *testing.T) {
111152
result := NewListFromSpec(test.specInfo)
112-
assert.Equal(t, test.expected, result)
153+
assert.ElementsMatch(t, test.expected, result)
113154
})
114155
}
115156
}
@@ -118,7 +159,7 @@ func TestNewExtensionsFrom2xxResponse(t *testing.T) {
118159
tests := []struct {
119160
name string
120161
responsesMap map[string]*openapi3.ResponseRef
121-
expected map[string]any
162+
expected []map[string]any
122163
}{
123164
{
124165
name: "Valid 200 response with extensions",
@@ -136,9 +177,11 @@ func TestNewExtensionsFrom2xxResponse(t *testing.T) {
136177
},
137178
},
138179
},
139-
expected: map[string]any{
140-
sunsetExtensionName: "2025-12-31",
141-
apiVersionExtensionName: "v1.0",
180+
expected: []map[string]any{
181+
{
182+
sunsetExtensionName: "2025-12-31",
183+
apiVersionExtensionName: "v1.0",
184+
},
142185
},
143186
},
144187
{
@@ -150,6 +193,34 @@ func TestNewExtensionsFrom2xxResponse(t *testing.T) {
150193
},
151194
expected: nil,
152195
},
196+
{
197+
name: "Content entry without sunset extension is skipped",
198+
responsesMap: map[string]*openapi3.ResponseRef{
199+
"200": {
200+
Value: &openapi3.Response{
201+
Content: openapi3.Content{
202+
"application/vnd.atlas.2023-01-01+json": &openapi3.MediaType{
203+
Extensions: map[string]any{
204+
sunsetExtensionName: "2025-12-31",
205+
apiVersionExtensionName: "2023-01-01",
206+
},
207+
},
208+
"application/vnd.atlas.2024-05-01+json": &openapi3.MediaType{
209+
Extensions: map[string]any{
210+
apiVersionExtensionName: "2024-05-01",
211+
},
212+
},
213+
},
214+
},
215+
},
216+
},
217+
expected: []map[string]any{
218+
{
219+
sunsetExtensionName: "2025-12-31",
220+
apiVersionExtensionName: "2023-01-01",
221+
},
222+
},
223+
},
153224
{
154225
name: "Empty extensions for 2xx response",
155226
responsesMap: map[string]*openapi3.ResponseRef{

0 commit comments

Comments
 (0)