← back to Cli Printing Press
fix(cli): handle wrapped paginated responses (#731)
b4bdcf68bc621c3a77e022b54f5ab43c6515a49f · 2026-05-08 09:43:41 -0700 · Trevin Chow
Files touched
M internal/generator/templates/helpers.go.tmplM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api/dogfood.jsonM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
Diff
commit b4bdcf68bc621c3a77e022b54f5ab43c6515a49f
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 8 09:43:41 2026 -0700
fix(cli): handle wrapped paginated responses (#731)
---
internal/generator/templates/helpers.go.tmpl | 64 ++++++++++++++++++----
.../internal/cli/helpers.go | 64 ++++++++++++++++++----
.../expected/generate-golden-api/dogfood.json | 2 +-
.../printing-press-golden/internal/cli/helpers.go | 64 ++++++++++++++++++----
4 files changed, 157 insertions(+), 37 deletions(-)
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 95f1f20b..969a6dcc 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -448,7 +448,7 @@ func paginatedGet(c interface {
}
// Fetch all pages
- var allItems []json.RawMessage
+ allItems := make([]json.RawMessage, 0)
page := 0
for {
page++
@@ -471,20 +471,13 @@ func paginatedGet(c interface {
// Response is an object - look for array inside
var obj map[string]json.RawMessage
if json.Unmarshal(data, &obj) == nil {
- // Try common data fields
- for _, field := range []string{"data", "items", "results", "messages", "members", "values"} {
- if arr, ok := obj[field]; ok {
- var nested []json.RawMessage
- if json.Unmarshal(arr, &nested) == nil {
- allItems = append(allItems, nested...)
- break
- }
- }
+ if nested, ok := extractPaginatedItems(obj); ok {
+ allItems = append(allItems, nested...)
}
// Check for next cursor
if nextCursorPath != "" {
- if tokenRaw, ok := obj[nextCursorPath]; ok {
+ if tokenRaw, ok := rawAtPath(obj, nextCursorPath); ok {
var token string
if json.Unmarshal(tokenRaw, &token) == nil && token != "" {
clean[cursorParam] = token
@@ -495,7 +488,7 @@ func paginatedGet(c interface {
// Check has_more
if hasMoreField != "" {
- if moreRaw, ok := obj[hasMoreField]; ok {
+ if moreRaw, ok := rawAtPath(obj, hasMoreField); ok {
var more bool
if json.Unmarshal(moreRaw, &more) == nil && more {
continue
@@ -520,6 +513,53 @@ func paginatedGet(c interface {
return json.RawMessage(result), nil
}
+func extractPaginatedItems(obj map[string]json.RawMessage) ([]json.RawMessage, bool) {
+ for _, field := range []string{"data", "items", "results", "messages", "members", "values"} {
+ if arr, ok := obj[field]; ok {
+ var nested []json.RawMessage
+ if json.Unmarshal(arr, &nested) == nil {
+ return nested, true
+ }
+ }
+ }
+
+ var onlyArray []json.RawMessage
+ arrayCount := 0
+ for _, raw := range obj {
+ var candidate []json.RawMessage
+ if json.Unmarshal(raw, &candidate) == nil {
+ onlyArray = candidate
+ arrayCount++
+ }
+ }
+ if arrayCount == 1 {
+ return onlyArray, true
+ }
+ return nil, false
+}
+
+func rawAtPath(obj map[string]json.RawMessage, path string) (json.RawMessage, bool) {
+ if raw, ok := obj[path]; ok {
+ return raw, true
+ }
+
+ current := obj
+ parts := strings.Split(path, ".")
+ for i, part := range parts {
+ raw, ok := current[part]
+ if !ok {
+ return nil, false
+ }
+ if i == len(parts)-1 {
+ return raw, true
+ }
+ if err := json.Unmarshal(raw, ¤t); err != nil {
+ return nil, false
+ }
+ }
+ return nil, false
+}
+
// printJSONFiltered marshals a Go-typed value through the same output
// pipeline endpoint-mirror commands use. Hand-written novel commands that
// build a typed slice/struct call this so --select, --compact, --csv, and
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
index 5910fdd7..5ed45def 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
@@ -285,7 +285,7 @@ func paginatedGet(c interface {
}
// Fetch all pages
- var allItems []json.RawMessage
+ allItems := make([]json.RawMessage, 0)
page := 0
for {
page++
@@ -308,20 +308,13 @@ func paginatedGet(c interface {
// Response is an object - look for array inside
var obj map[string]json.RawMessage
if json.Unmarshal(data, &obj) == nil {
- // Try common data fields
- for _, field := range []string{"data", "items", "results", "messages", "members", "values"} {
- if arr, ok := obj[field]; ok {
- var nested []json.RawMessage
- if json.Unmarshal(arr, &nested) == nil {
- allItems = append(allItems, nested...)
- break
- }
- }
+ if nested, ok := extractPaginatedItems(obj); ok {
+ allItems = append(allItems, nested...)
}
// Check for next cursor
if nextCursorPath != "" {
- if tokenRaw, ok := obj[nextCursorPath]; ok {
+ if tokenRaw, ok := rawAtPath(obj, nextCursorPath); ok {
var token string
if json.Unmarshal(tokenRaw, &token) == nil && token != "" {
clean[cursorParam] = token
@@ -332,7 +325,7 @@ func paginatedGet(c interface {
// Check has_more
if hasMoreField != "" {
- if moreRaw, ok := obj[hasMoreField]; ok {
+ if moreRaw, ok := rawAtPath(obj, hasMoreField); ok {
var more bool
if json.Unmarshal(moreRaw, &more) == nil && more {
continue
@@ -357,6 +350,53 @@ func paginatedGet(c interface {
return json.RawMessage(result), nil
}
+func extractPaginatedItems(obj map[string]json.RawMessage) ([]json.RawMessage, bool) {
+ for _, field := range []string{"data", "items", "results", "messages", "members", "values"} {
+ if arr, ok := obj[field]; ok {
+ var nested []json.RawMessage
+ if json.Unmarshal(arr, &nested) == nil {
+ return nested, true
+ }
+ }
+ }
+
+ var onlyArray []json.RawMessage
+ arrayCount := 0
+ for _, raw := range obj {
+ var candidate []json.RawMessage
+ if json.Unmarshal(raw, &candidate) == nil {
+ onlyArray = candidate
+ arrayCount++
+ }
+ }
+ if arrayCount == 1 {
+ return onlyArray, true
+ }
+ return nil, false
+}
+
+func rawAtPath(obj map[string]json.RawMessage, path string) (json.RawMessage, bool) {
+ if raw, ok := obj[path]; ok {
+ return raw, true
+ }
+
+ current := obj
+ parts := strings.Split(path, ".")
+ for i, part := range parts {
+ raw, ok := current[part]
+ if !ok {
+ return nil, false
+ }
+ if i == len(parts)-1 {
+ return raw, true
+ }
+ if err := json.Unmarshal(raw, ¤t); err != nil {
+ return nil, false
+ }
+ }
+ return nil, false
+}
+
// printJSONFiltered marshals a Go-typed value through the same output
// pipeline endpoint-mirror commands use. Hand-written novel commands that
// build a typed slice/struct call this so --select, --compact, --csv, and
diff --git a/testdata/golden/expected/generate-golden-api/dogfood.json b/testdata/golden/expected/generate-golden-api/dogfood.json
index 8e10242c..c144e26f 100644
--- a/testdata/golden/expected/generate-golden-api/dogfood.json
+++ b/testdata/golden/expected/generate-golden-api/dogfood.json
@@ -16,7 +16,7 @@
},
"dead_functions": {
"dead": 0,
- "total": 51
+ "total": 53
},
"dir": "<ARTIFACT_DIR>/generate-golden-api/printing-press-golden",
"example_check": {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
index e8ebdddf..80b431b3 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
@@ -288,7 +288,7 @@ func paginatedGet(c interface {
}
// Fetch all pages
- var allItems []json.RawMessage
+ allItems := make([]json.RawMessage, 0)
page := 0
for {
page++
@@ -311,20 +311,13 @@ func paginatedGet(c interface {
// Response is an object - look for array inside
var obj map[string]json.RawMessage
if json.Unmarshal(data, &obj) == nil {
- // Try common data fields
- for _, field := range []string{"data", "items", "results", "messages", "members", "values"} {
- if arr, ok := obj[field]; ok {
- var nested []json.RawMessage
- if json.Unmarshal(arr, &nested) == nil {
- allItems = append(allItems, nested...)
- break
- }
- }
+ if nested, ok := extractPaginatedItems(obj); ok {
+ allItems = append(allItems, nested...)
}
// Check for next cursor
if nextCursorPath != "" {
- if tokenRaw, ok := obj[nextCursorPath]; ok {
+ if tokenRaw, ok := rawAtPath(obj, nextCursorPath); ok {
var token string
if json.Unmarshal(tokenRaw, &token) == nil && token != "" {
clean[cursorParam] = token
@@ -335,7 +328,7 @@ func paginatedGet(c interface {
// Check has_more
if hasMoreField != "" {
- if moreRaw, ok := obj[hasMoreField]; ok {
+ if moreRaw, ok := rawAtPath(obj, hasMoreField); ok {
var more bool
if json.Unmarshal(moreRaw, &more) == nil && more {
continue
@@ -360,6 +353,53 @@ func paginatedGet(c interface {
return json.RawMessage(result), nil
}
+func extractPaginatedItems(obj map[string]json.RawMessage) ([]json.RawMessage, bool) {
+ for _, field := range []string{"data", "items", "results", "messages", "members", "values"} {
+ if arr, ok := obj[field]; ok {
+ var nested []json.RawMessage
+ if json.Unmarshal(arr, &nested) == nil {
+ return nested, true
+ }
+ }
+ }
+
+ var onlyArray []json.RawMessage
+ arrayCount := 0
+ for _, raw := range obj {
+ var candidate []json.RawMessage
+ if json.Unmarshal(raw, &candidate) == nil {
+ onlyArray = candidate
+ arrayCount++
+ }
+ }
+ if arrayCount == 1 {
+ return onlyArray, true
+ }
+ return nil, false
+}
+
+func rawAtPath(obj map[string]json.RawMessage, path string) (json.RawMessage, bool) {
+ if raw, ok := obj[path]; ok {
+ return raw, true
+ }
+
+ current := obj
+ parts := strings.Split(path, ".")
+ for i, part := range parts {
+ raw, ok := current[part]
+ if !ok {
+ return nil, false
+ }
+ if i == len(parts)-1 {
+ return raw, true
+ }
+ if err := json.Unmarshal(raw, ¤t); err != nil {
+ return nil, false
+ }
+ }
+ return nil, false
+}
+
// printJSONFiltered marshals a Go-typed value through the same output
// pipeline endpoint-mirror commands use. Hand-written novel commands that
// build a typed slice/struct call this so --select, --compact, --csv, and
← 979510c2 fix(cli): score structural scorer behavior (#732)
·
back to Cli Printing Press
·
fix(cli): guard generated CLI build safety (#736) 1d26ae77 →