Skip to content

Commit a2c52a4

Browse files
committed
Rework after code review
1 parent accbc99 commit a2c52a4

3 files changed

Lines changed: 244 additions & 225 deletions

File tree

‎internal/cmd/alpha/folder/describe/describe_test.go‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import (
1515
"github.com/stackitcloud/stackit-cli/internal/pkg/testutils"
1616
)
1717

18-
var containerIdFlag = globalflags.ProjectIdFlag
19-
2018
type testCtxKey struct{}
2119

2220
var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo")
@@ -93,7 +91,7 @@ func TestParseInput(t *testing.T) {
9391
description: "container id arg takes precedence",
9492
argValues: fixtureArgValues(),
9593
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
96-
flagValues[containerIdFlag] = testFolderId2
94+
flagValues[globalflags.ProjectIdFlag] = testFolderId2
9795
}),
9896
isValid: true,
9997
expectedModel: fixtureInputModel(func(model *inputModel) {
@@ -104,7 +102,7 @@ func TestParseInput(t *testing.T) {
104102
description: "container id arg missing",
105103
argValues: []string{},
106104
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
107-
flagValues[containerIdFlag] = testFolderId
105+
flagValues[globalflags.ProjectIdFlag] = testFolderId
108106
}),
109107
isValid: true,
110108
expectedModel: fixtureInputModel(func(model *inputModel) {
@@ -115,21 +113,21 @@ func TestParseInput(t *testing.T) {
115113
description: "project id missing",
116114
argValues: []string{},
117115
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
118-
delete(flagValues, containerIdFlag)
116+
delete(flagValues, globalflags.ProjectIdFlag)
119117
}),
120118
isValid: false,
121119
},
122120
{
123121
description: "container id invalid 1",
124122
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
125-
flagValues[containerIdFlag] = ""
123+
flagValues[globalflags.ProjectIdFlag] = ""
126124
}),
127125
isValid: false,
128126
},
129127
{
130128
description: "container id invalid 2",
131129
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
132-
flagValues[containerIdFlag] = "invalid-uuid"
130+
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
133131
}),
134132
isValid: false,
135133
},

‎internal/cmd/alpha/folder/list/list.go‎

Lines changed: 89 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ package list
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"strconv"
68
"time"
79

810
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
911

1012
"github.com/spf13/cobra"
13+
"github.com/stackitcloud/stackit-sdk-go/core/experimental/paginate"
1114
resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api"
1215

1316
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
1417
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
15-
"github.com/stackitcloud/stackit-cli/internal/pkg/errors"
18+
internalerrors "github.com/stackitcloud/stackit-cli/internal/pkg/errors"
1619
"github.com/stackitcloud/stackit-cli/internal/pkg/examples"
1720
"github.com/stackitcloud/stackit-cli/internal/pkg/flags"
1821
"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
@@ -95,51 +98,42 @@ func configureFlags(cmd *cobra.Command) {
9598
cmd.Flags().String(memberFlag, "", "Filter by member. The list of folders of which the member is part of will be shown")
9699
cmd.Flags().String(creationTimeAfterFlag, "", "Filter by creation timestamp, in a date-time with the RFC3339 layout format, e.g. 2023-01-01T00:00:00Z. The list of folders that were created after the given timestamp will be shown")
97100
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
98-
cmd.Flags().Int64(pageSizeFlag, pageSizeDefault, "Number of items fetched in each API call. Does not affect the number of items in the command output")
99101
}
100102

101103
func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
102104
globalFlags := globalflags.Parse(p, cmd)
103105

104106
creationTimeAfter, err := flags.FlagToDateTimePointer(p, cmd, creationTimeAfterFlag, creationTimeAfterFormat)
105107
if err != nil {
106-
return nil, &errors.FlagValidationError{
108+
return nil, &internalerrors.FlagValidationError{
107109
Flag: creationTimeAfterFlag,
108110
Details: err.Error(),
109111
}
110112
}
111113

112114
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
113115
if limit != nil && *limit < 1 {
114-
return nil, &errors.FlagValidationError{
116+
return nil, &internalerrors.FlagValidationError{
115117
Flag: limitFlag,
116118
Details: "must be greater than 0",
117119
}
118120
}
119121

120-
pageSize := flags.FlagWithDefaultToInt64Value(p, cmd, pageSizeFlag)
121-
if pageSize < 1 {
122-
return nil, &errors.FlagValidationError{
123-
Flag: pageSizeFlag,
124-
Details: "must be greater than 0",
125-
}
126-
}
127-
128122
model := inputModel{
129123
GlobalFlagModel: globalFlags,
130124
ParentId: flags.FlagToStringPointer(p, cmd, parentIdFlag),
131125
ProjectIdLike: flags.FlagToStringSliceValue(p, cmd, projectIdLikeFlag),
132126
Member: flags.FlagToStringPointer(p, cmd, memberFlag),
133127
CreationTimeAfter: creationTimeAfter,
134128
Limit: limit,
135-
PageSize: pageSize,
129+
PageSize: pageSizeDefault,
136130
}
137131

138132
p.DebugInputModel(model)
139133
return &model, nil
140134
}
141135

142-
func buildRequest(ctx context.Context, model *inputModel, apiClient resourceManagerClient, offset int) (resourcemanager.ApiListFoldersRequest, error) {
136+
func buildRequest(ctx context.Context, model *inputModel, apiClient resourcemanager.DefaultAPI) (resourcemanager.ApiListFoldersRequest, error) {
143137
req := apiClient.ListFolders(ctx)
144138
if model.ParentId != nil {
145139
req = req.ContainerParentId(*model.ParentId)
@@ -150,6 +144,9 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient resourceMana
150144
if model.CreationTimeAfter != nil {
151145
req = req.CreationTimeStart(*model.CreationTimeAfter)
152146
}
147+
if model.ProjectIdLike != nil {
148+
req = req.ContainerIds(model.ProjectIdLike)
149+
}
153150

154151
if model.ParentId == nil && model.ProjectIdLike == nil && model.Member == nil {
155152
email, err := auth.GetAuthEmail()
@@ -159,49 +156,28 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient resourceMana
159156
req = req.Member(email)
160157
}
161158
req = req.Limit(float32(model.PageSize))
162-
req = req.Offset(float32(offset))
163159
return req, nil
164160
}
165161

166-
type resourceManagerClient interface {
167-
ListFolders(ctx context.Context) resourcemanager.ApiListFoldersRequest
168-
}
162+
func fetchFolders(ctx context.Context, model *inputModel, apiClient resourcemanager.DefaultAPI) ([]resourcemanager.ListFoldersResponseItemsInner, error) {
163+
opts := []paginate.Option{paginate.WithPageSize(pageSizeDefault)}
164+
if model.Limit != nil {
165+
opts = append(opts, paginate.WithLimit(int(*model.Limit)))
166+
}
169167

170-
func fetchFolders(ctx context.Context, model *inputModel, apiClient resourceManagerClient) ([]resourcemanager.ListFoldersResponseItemsInner, error) {
171-
if model.Limit != nil && *model.Limit < model.PageSize {
172-
model.PageSize = *model.Limit
168+
req, err := buildRequest(ctx, model, apiClient)
169+
if err != nil {
170+
return nil, fmt.Errorf("failed to build request: %w", err)
173171
}
174172

175-
offset := 0
176-
folders := []resourcemanager.ListFoldersResponseItemsInner{}
177-
for {
178-
// Call API
179-
req, err := buildRequest(ctx, model, apiClient, offset)
180-
if err != nil {
181-
return nil, fmt.Errorf("build list folders request: %w", err)
182-
}
183-
resp, err := req.Execute()
173+
items := make([]resourcemanager.ListFoldersResponseItemsInner, 0)
174+
for item, err := range paginate.Items(&listFoldersAdapter{request: req}, opts...) {
184175
if err != nil {
185-
return nil, fmt.Errorf("get folders: %w", err)
186-
}
187-
respFolders := resp.GetItems()
188-
if len(respFolders) == 0 {
189-
break
190-
}
191-
folders = append(folders, respFolders...)
192-
// Stop if no more pages
193-
if len(respFolders) < int(model.PageSize) {
194-
break
195-
}
196-
197-
// Stop and truncate if limit is reached
198-
if model.Limit != nil && len(folders) >= int(*model.Limit) {
199-
folders = folders[:*model.Limit]
200-
break
176+
return nil, fmt.Errorf("list items: %w", err)
201177
}
202-
offset += int(model.PageSize)
178+
items = append(items, item)
203179
}
204-
return folders, nil
180+
return items, nil
205181
}
206182

207183
func outputResult(p *print.Printer, outputFormat string, folders []resourcemanager.ListFoldersResponseItemsInner) error {
@@ -212,9 +188,7 @@ func outputResult(p *print.Printer, outputFormat string, folders []resourcemanag
212188
}
213189
table := tables.NewTable()
214190
table.SetHeader("ID", "NAME", "PARENT ID")
215-
for i := range folders {
216-
f := folders[i]
217-
191+
for _, f := range folders {
218192
table.AddRow(
219193
f.FolderId,
220194
f.Name,
@@ -230,3 +204,67 @@ func outputResult(p *print.Printer, outputFormat string, folders []resourcemanag
230204
return nil
231205
})
232206
}
207+
208+
var (
209+
_ paginate.Request[*listFoldersAdapter, listFoldersAdapterResponse] = (*listFoldersAdapter)(nil)
210+
_ paginate.Response[resourcemanager.ListFoldersResponseItemsInner] = listFoldersAdapterResponse{}
211+
)
212+
213+
// listFoldersAdapter maps the token-based paginate API onto the
214+
// offset/limit-based ListFolders endpoint. The page token is the
215+
// decimal offset of the next page.
216+
type listFoldersAdapter struct {
217+
request resourcemanager.ApiListFoldersRequest
218+
pageSize int32
219+
offset int64
220+
err error
221+
}
222+
223+
func (a *listFoldersAdapter) PageSize(pageSize int32) *listFoldersAdapter {
224+
a.pageSize = pageSize
225+
a.request = a.request.Limit(float32(pageSize))
226+
return a
227+
}
228+
229+
func (a *listFoldersAdapter) PageToken(pageToken string) *listFoldersAdapter {
230+
offset, err := strconv.ParseInt(pageToken, 10, 64)
231+
if err != nil || offset < 0 {
232+
a.err = fmt.Errorf("invalid page token %q", pageToken)
233+
return a
234+
}
235+
a.offset = offset
236+
a.request = a.request.Offset(float32(offset))
237+
return a
238+
}
239+
240+
func (a *listFoldersAdapter) Execute() (listFoldersAdapterResponse, error) {
241+
if a.err != nil {
242+
return listFoldersAdapterResponse{}, a.err
243+
}
244+
resp, err := a.request.Execute()
245+
if err != nil {
246+
return listFoldersAdapterResponse{}, err
247+
}
248+
if resp == nil {
249+
return listFoldersAdapterResponse{}, errors.New("empty response from API")
250+
}
251+
return listFoldersAdapterResponse{
252+
ListFoldersResponse: resp,
253+
offset: a.offset,
254+
pageSize: a.pageSize,
255+
}, nil
256+
}
257+
258+
type listFoldersAdapterResponse struct {
259+
*resourcemanager.ListFoldersResponse
260+
offset int64
261+
pageSize int32
262+
}
263+
264+
func (r listFoldersAdapterResponse) GetNextPageToken() string {
265+
n := int64(len(r.GetItems()))
266+
if n == 0 || n < int64(r.pageSize) {
267+
return ""
268+
}
269+
return strconv.FormatInt(r.offset+n, 10)
270+
}

0 commit comments

Comments
 (0)