Skip to content

Commit 8b7d264

Browse files
committed
Add JSON pagination envelope to projects list
1 parent 29d5d69 commit 8b7d264

2 files changed

Lines changed: 80 additions & 11 deletions

File tree

cmd/projects.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"net/http"
78
"strconv"
@@ -42,6 +43,7 @@ type ProjectsCmd struct {
4243
type ProjectsListInput struct {
4344
Limit int
4445
Offset int
46+
Output string
4547
}
4648

4749
type ProjectsCreateInput struct {
@@ -92,6 +94,9 @@ func resolveProjectArg(ctx context.Context, projects ProjectListService, val str
9294
}
9395

9496
func (c ProjectsCmd) List(ctx context.Context, in ProjectsListInput) error {
97+
if err := validateJSONOutput(in.Output); err != nil {
98+
return err
99+
}
95100
if in.Limit < 1 || in.Limit > 100 {
96101
return fmt.Errorf("--limit must be between 1 and 100")
97102
}
@@ -108,13 +113,37 @@ func (c ProjectsCmd) List(ctx context.Context, in ProjectsListInput) error {
108113
return util.CleanedUpSdkError{Err: err}
109114
}
110115

111-
if projects == nil || len(projects.Items) == 0 {
116+
items := []kernel.Project{}
117+
if projects != nil {
118+
items = projects.Items
119+
}
120+
121+
nextOffset := projectListNextOffsetRaw(response)
122+
if in.Output == "json" {
123+
payload := struct {
124+
Projects []kernel.Project `json:"projects"`
125+
NextOffset string `json:"next_offset,omitempty"`
126+
}{
127+
Projects: items,
128+
}
129+
if nextOffset != "" {
130+
payload.NextOffset = nextOffset
131+
}
132+
data, err := json.MarshalIndent(payload, "", " ")
133+
if err != nil {
134+
return err
135+
}
136+
fmt.Println(string(data))
137+
return nil
138+
}
139+
140+
if len(items) == 0 {
112141
pterm.Info.Println("No projects found")
113142
return nil
114143
}
115144

116145
table := pterm.TableData{{"ID", "Name", "Status", "Created At", "idx"}}
117-
for i, p := range projects.Items {
146+
for i, p := range items {
118147
table = append(table, []string{
119148
p.ID,
120149
p.Name,
@@ -125,20 +154,24 @@ func (c ProjectsCmd) List(ctx context.Context, in ProjectsListInput) error {
125154
}
126155
PrintTableNoPad(table, true)
127156

128-
if nextOffset, ok := projectListNextOffset(response); ok {
157+
if nextOffset, ok := projectListNextOffset(nextOffset); ok {
129158
pterm.Warning.Printfln(
130159
"Output truncated after index %d. Continue with: kernel projects list --limit %d --offset %d",
131-
in.Offset+len(projects.Items)-1, in.Limit, nextOffset,
160+
in.Offset+len(items)-1, in.Limit, nextOffset,
132161
)
133162
}
134163
return nil
135164
}
136165

137-
func projectListNextOffset(response *http.Response) (int, bool) {
166+
func projectListNextOffsetRaw(response *http.Response) string {
138167
if response == nil {
139-
return 0, false
168+
return ""
140169
}
141-
nextOffset, err := strconv.Atoi(response.Header.Get("X-Next-Offset"))
170+
return strings.TrimSpace(response.Header.Get("X-Next-Offset"))
171+
}
172+
173+
func projectListNextOffset(nextOffsetRaw string) (int, bool) {
174+
nextOffset, err := strconv.Atoi(nextOffsetRaw)
142175
return nextOffset, err == nil && nextOffset > 0
143176
}
144177

@@ -362,7 +395,8 @@ func runProjectsList(cmd *cobra.Command, args []string) error {
362395
c := getProjectsHandler(cmd)
363396
limit, _ := cmd.Flags().GetInt("limit")
364397
offset, _ := cmd.Flags().GetInt("offset")
365-
return c.List(cmd.Context(), ProjectsListInput{Limit: limit, Offset: offset})
398+
output, _ := cmd.Flags().GetString("output")
399+
return c.List(cmd.Context(), ProjectsListInput{Limit: limit, Offset: offset, Output: output})
366400
}
367401

368402
func runProjectsCreate(cmd *cobra.Command, args []string) error {
@@ -518,6 +552,7 @@ var projectsSetLimitsCompatCmd = &cobra.Command{
518552
func init() {
519553
projectsListCmd.Flags().Int("limit", 100, "Maximum number of projects to return (1-100)")
520554
projectsListCmd.Flags().Int("offset", 0, "Number of projects to skip (for pagination)")
555+
addJSONOutputFlag(projectsListCmd)
521556

522557
projectsUpdateCmd.Flags().String("name", "", "New project name (1-255 characters)")
523558
projectsUpdateCmd.Flags().String("status", "", "New project status: active or archived")

cmd/projects_test.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"net/http"
78
"testing"
@@ -103,6 +104,36 @@ func TestProjectsList_ForwardsPaginationAndShowsAbsoluteIndexes(t *testing.T) {
103104
assert.Contains(t, out, "21")
104105
}
105106

107+
func TestProjectsList_JSONOutputEnvelope(t *testing.T) {
108+
fakeProjects := &FakeProjectsService{
109+
ListFunc: func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) {
110+
return &pagination.OffsetPagination[kernel.Project]{
111+
Items: []kernel.Project{
112+
{ID: "proj_1", Name: "one", Status: kernel.ProjectStatusActive},
113+
{ID: "proj_2", Name: "two", Status: kernel.ProjectStatusArchived},
114+
},
115+
}, nil
116+
},
117+
}
118+
c := ProjectsCmd{projects: fakeProjects, limits: &FakeProjectLimitsService{}}
119+
120+
out := captureStdout(t, func() {
121+
err := c.List(context.Background(), ProjectsListInput{Limit: 2, Offset: 0, Output: "json"})
122+
assert.NoError(t, err)
123+
})
124+
125+
var payload struct {
126+
Projects []kernel.Project `json:"projects"`
127+
NextOffset string `json:"next_offset"`
128+
}
129+
if !assert.NoError(t, json.Unmarshal([]byte(out), &payload)) {
130+
return
131+
}
132+
assert.Len(t, payload.Projects, 2)
133+
assert.Equal(t, "proj_1", payload.Projects[0].ID)
134+
assert.Empty(t, payload.NextOffset)
135+
}
136+
106137
func TestProjectsList_RejectsInvalidPagination(t *testing.T) {
107138
fakeProjects := &FakeProjectsService{
108139
ListFunc: func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) {
@@ -123,13 +154,16 @@ func TestProjectsList_RejectsInvalidPagination(t *testing.T) {
123154

124155
func TestProjectListNextOffset(t *testing.T) {
125156
response := &http.Response{Header: http.Header{"X-Next-Offset": []string{"120"}}}
126-
nextOffset, ok := projectListNextOffset(response)
157+
nextOffsetRaw := projectListNextOffsetRaw(response)
158+
assert.Equal(t, "120", nextOffsetRaw)
159+
160+
nextOffset, ok := projectListNextOffset(nextOffsetRaw)
127161
assert.True(t, ok)
128162
assert.Equal(t, 120, nextOffset)
129163

130-
_, ok = projectListNextOffset(&http.Response{Header: http.Header{}})
164+
_, ok = projectListNextOffset(projectListNextOffsetRaw(&http.Response{Header: http.Header{}}))
131165
assert.False(t, ok)
132-
_, ok = projectListNextOffset(nil)
166+
_, ok = projectListNextOffset(projectListNextOffsetRaw(nil))
133167
assert.False(t, ok)
134168
}
135169

0 commit comments

Comments
 (0)