Skip to content

Commit 75ec4a0

Browse files
fix: Fix foascli sunset list to print sunset endpoints with deterministic order (#804)
1 parent f4087b2 commit 75ec4a0

File tree

3 files changed

+157
-5
lines changed

3 files changed

+157
-5
lines changed

tools/cli/internal/cli/sunset/list.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package sunset
1717
import (
1818
"encoding/json"
1919
"fmt"
20+
"sort"
2021
"strings"
2122
"time"
2223

@@ -52,6 +53,14 @@ func (o *ListOpts) Run() error {
5253
return err
5354
}
5455

56+
// order sunset elements per Path,Operation in ascending order
57+
sort.Slice(sunsets, func(i, j int) bool {
58+
if sunsets[i].Path != sunsets[j].Path {
59+
return sunsets[i].Path < sunsets[j].Path
60+
}
61+
return sunsets[i].Operation < sunsets[j].Operation
62+
})
63+
5564
bytes, err := o.newSunsetListBytes(sunsets)
5665
if err != nil {
5766
return err
@@ -170,6 +179,5 @@ func ListBuilder() *cobra.Command {
170179
cmd.Flags().StringVarP(&opts.format, flag.Format, flag.FormatShort, "json", usage.Format)
171180

172181
_ = cmd.MarkFlagRequired(flag.Spec)
173-
174182
return cmd
175183
}

tools/cli/internal/cli/sunset/list_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
package sunset
1616

1717
import (
18+
"encoding/json"
19+
"reflect"
1820
"testing"
1921

22+
"github.com/mongodb/openapi/tools/cli/internal/openapi/sunset"
2023
"github.com/spf13/afero"
2124
"github.com/stretchr/testify/assert"
2225
"github.com/stretchr/testify/require"
@@ -28,10 +31,106 @@ func TestList_Run(t *testing.T) {
2831
basePath: "../../../test/data/base_spec.json",
2932
outputPath: "foas.json",
3033
fs: fs,
34+
format: "json",
35+
from: "2024-09-22",
36+
to: "2026-09-22",
3137
}
3238

3339
require.NoError(t, opts.Run())
3440
b, err := afero.ReadFile(fs, opts.outputPath)
3541
require.NoError(t, err)
3642
assert.NotEmpty(t, b)
43+
var results []*sunset.Sunset
44+
require.NoError(t, json.Unmarshal(b, &results))
45+
if !reflect.DeepEqual(results, expectedResults) {
46+
gotPretty, _ := json.MarshalIndent(results, "", " ")
47+
wantPretty, _ := json.MarshalIndent(expectedResults, "", " ")
48+
t.Errorf("mismatch:\nGot:\n%s\nWant:\n%s", string(gotPretty), string(wantPretty))
49+
}
50+
}
51+
52+
var expectedResults = []*sunset.Sunset{
53+
{Operation: "GET", Path: "/api/atlas/v2/example/info", SunsetDate: "2025-06-01", Team: "APIx",
54+
Version: "2023-01-01"},
55+
{Operation: "GET",
56+
Path: "/api/atlas/v2/federationSettings/{federationSettingsId}/identityProviders/{identityProviderId}",
57+
SunsetDate: "2025-01-01", Team: "IAM", Version: "2023-01-01"},
58+
{Operation: "PATCH",
59+
Path: "/api/atlas/v2/federationSettings/{federationSettingsId}/identityProviders/{identityProviderId}",
60+
SunsetDate: "2025-01-01", Team: "IAM", Version: "2023-01-01"},
61+
{Operation: "PATCH", Path: "/api/atlas/v2/groups/{groupId}/alerts/{alertId}", SunsetDate: "2025-05-30",
62+
Team: "CAP", Version: "2023-01-01"},
63+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/backup/exportBuckets", SunsetDate: "2025-05-30",
64+
Team: "Backup - Atlas", Version: "2023-01-01"},
65+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/backup/exportBuckets",
66+
SunsetDate: "2025-05-30", Team: "Backup - Atlas", Version: "2023-01-01"},
67+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/backup/exportBuckets/{exportBucketId}",
68+
SunsetDate: "2025-05-30", Team: "Backup - Atlas", Version: "2023-01-01"},
69+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/backupCompliancePolicy",
70+
SunsetDate: "2024-10-01", Team: "Backup - Atlas", Version: "2023-01-01"},
71+
{Operation: "PUT", Path: "/api/atlas/v2/groups/{groupId}/backupCompliancePolicy",
72+
SunsetDate: "2024-10-01", Team: "Backup - Atlas", Version: "2023-01-01"},
73+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/clusters",
74+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
75+
{Operation: "DELETE", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
76+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
77+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
78+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
79+
{Operation: "PATCH", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
80+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
81+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes",
82+
SunsetDate: "2025-06-01", Team: "Search Web Platform", Version: "2023-01-01"},
83+
{Operation: "GET",
84+
Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{databaseName}/{collectionName}",
85+
SunsetDate: "2025-06-01", Team: "Search Web Platform", Version: "2023-01-01"},
86+
{Operation: "DELETE",
87+
Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}",
88+
SunsetDate: "2025-06-01", Team: "Search Web Platform", Version: "2023-01-01"},
89+
{Operation: "GET",
90+
Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}",
91+
SunsetDate: "2025-06-01", Team: "Search Web Platform", Version: "2023-01-01"},
92+
{Operation: "PATCH", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/fts/indexes/{indexId}",
93+
SunsetDate: "2025-06-01", Team: "Search Web Platform", Version: "2023-01-01"},
94+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites",
95+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
96+
{Operation: "DELETE", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping",
97+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
98+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/customZoneMapping",
99+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
100+
{Operation: "DELETE", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces",
101+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
102+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/globalWrites/managedNamespaces",
103+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
104+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs",
105+
SunsetDate: "2025-06-01", Team: "Atlas", Version: "2023-01-01"},
106+
{Operation: "PATCH", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/processArgs",
107+
SunsetDate: "2025-06-01", Team: "Atlas", Version: "2023-01-01"},
108+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/restartPrimaries",
109+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
110+
{Operation: "DELETE", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/search/deployment",
111+
SunsetDate: "2026-03-01", Team: "Search Web Platform", Version: "2023-01-01"},
112+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/search/deployment",
113+
SunsetDate: "2026-03-01", Team: "Search Web Platform", Version: "2023-01-01"},
114+
{Operation: "PATCH", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/search/deployment",
115+
SunsetDate: "2026-03-01", Team: "Search Web Platform", Version: "2023-01-01"},
116+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/clusters/{clusterName}/search/deployment",
117+
SunsetDate: "2026-03-01", Team: "Search Web Platform", Version: "2023-01-01"},
118+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/clusters/{hostName}/logs/{logName}.gz",
119+
SunsetDate: "2025-06-01", Team: "Atlas Dedicated", Version: "2023-01-01"},
120+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/invites", SunsetDate: "2024-10-04", Team: "IAM",
121+
Version: "2023-01-01"},
122+
{Operation: "PATCH", Path: "/api/atlas/v2/groups/{groupId}/invites",
123+
SunsetDate: "2024-10-04", Team: "IAM", Version: "2023-01-01"},
124+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/invites", SunsetDate: "2024-10-04", Team: "IAM",
125+
Version: "2023-01-01"},
126+
{Operation: "DELETE", Path: "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", SunsetDate: "2024-10-04",
127+
Team: "IAM", Version: "2023-01-01"},
128+
{Operation: "GET", Path: "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", SunsetDate: "2024-10-04",
129+
Team: "IAM", Version: "2023-01-01"},
130+
{Operation: "PATCH", Path: "/api/atlas/v2/groups/{groupId}/invites/{invitationId}", SunsetDate: "2024-10-04",
131+
Team: "IAM", Version: "2023-01-01"},
132+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/liveMigrations", SunsetDate: "2025-05-30",
133+
Team: "Atlas Migrations", Version: "2023-01-01"},
134+
{Operation: "POST", Path: "/api/atlas/v2/groups/{groupId}/liveMigrations/validate", SunsetDate: "2025-05-30",
135+
Team: "Atlas Migrations", Version: "2023-01-01"},
37136
}

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
package sunset
1616

1717
import (
18+
"maps"
19+
"regexp"
20+
"slices"
21+
"sort"
22+
1823
"github.com/getkin/kin-openapi/openapi3"
1924
"github.com/tufin/oasdiff/load"
2025
)
@@ -77,6 +82,19 @@ func teamName(op *openapi3.Operation) string {
7782
return ""
7883
}
7984

85+
// successResponseExtensions searches through a map of response objects for successful HTTP status
86+
// codes (200, 201, 202, 204) and returns the extensions from the content of the first successful
87+
// response found.
88+
//
89+
// The function prioritizes responses in the following order: 200, 201, 202, 204. For each found
90+
// response, it extracts extensions from its content using the contentExtensions helper function.
91+
//
92+
// Parameters:
93+
// - responsesMap: A map of HTTP status codes to OpenAPI response objects
94+
//
95+
// Returns:
96+
// - A map of extension names to their values from the first successful response content,
97+
// or nil if no successful responses are found or if none contain relevant extensions
8098
func successResponseExtensions(responsesMap map[string]*openapi3.ResponseRef) map[string]any {
8199
if val, ok := responsesMap["200"]; ok {
82100
return contentExtensions(val.Value.Content)
@@ -94,9 +112,36 @@ func successResponseExtensions(responsesMap map[string]*openapi3.ResponseRef) ma
94112
return nil
95113
}
96114

115+
// contentExtensions extracts extensions from OpenAPI content objects, prioritizing content entries
116+
// with the oldest date in their keys.
117+
//
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+
//
121+
// Parameters:
122+
// - content: An OpenAPI content map with media types as keys and schema objects as values
123+
//
124+
// 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.
97129
func contentExtensions(content openapi3.Content) map[string]any {
98-
for _, v := range content {
99-
return v.Extensions
100-
}
101-
return nil
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
141+
}
142+
// Strings with dates should come before those without.
143+
return dateI != ""
144+
})
145+
146+
return content[keysContent[0]].Extensions
102147
}

0 commit comments

Comments
 (0)