[object Object]

← back to Cli Printing Press

fix(cli): handle wrapped paginated responses (#731)

b4bdcf68bc621c3a77e022b54f5ab43c6515a49f · 2026-05-08 09:43:41 -0700 · Trevin Chow

Files touched

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, &current); 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, &current); 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, &current); 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 →