← back to Cli Printing Press
fix(cli): preserve OpenAPI param casing in detected pagination (#1460)
272d8e835a5513f1bef2af2b507462954bbcfba6 · 2026-05-15 13:14:56 -0700 · Trevin Chow
detectPagination built a case-insensitive lookup map but then stored the
candidate lookup key (always lowercase) as Pagination.LimitParam /
CursorParam. Specs declaring camelCase params like pageSize, pageToken,
or maxResults shipped a sync that sent the lowercased form, which
Google APIs reject with HTTP 400 ("Unknown name 'pagesize'").
Switch the lookup to a lowercase->original map and store the spec's
original casing. Test matrix covers pageSize/page_size/limit/maxResults
plus the cursor variants pageToken/page_token/after.
Closes #1353
Files touched
M internal/openapi/parser.goM internal/openapi/parser_test.go
Diff
commit 272d8e835a5513f1bef2af2b507462954bbcfba6
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 15 13:14:56 2026 -0700
fix(cli): preserve OpenAPI param casing in detected pagination (#1460)
detectPagination built a case-insensitive lookup map but then stored the
candidate lookup key (always lowercase) as Pagination.LimitParam /
CursorParam. Specs declaring camelCase params like pageSize, pageToken,
or maxResults shipped a sync that sent the lowercased form, which
Google APIs reject with HTTP 400 ("Unknown name 'pagesize'").
Switch the lookup to a lowercase->original map and store the spec's
original casing. Test matrix covers pageSize/page_size/limit/maxResults
plus the cursor variants pageToken/page_token/after.
Closes #1353
---
internal/openapi/parser.go | 23 ++++++++++--------
internal/openapi/parser_test.go | 52 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 65 insertions(+), 10 deletions(-)
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index 1f9a084e..c94c118b 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -5034,25 +5034,28 @@ func selectDescription(summary, description string) string {
}
func detectPagination(params []spec.Param, op *openapi3.Operation) *spec.Pagination {
- paramNames := map[string]struct{}{}
+ // Map lowercase parameter name back to the spec's original casing so
+ // the detected LimitParam/CursorParam preserves whatever the API
+ // expects (e.g. Google APIs reject `pagesize` but accept `pageSize`).
+ originalCase := map[string]string{}
for _, p := range params {
- paramNames[strings.ToLower(p.Name)] = struct{}{}
+ originalCase[strings.ToLower(p.Name)] = p.Name
}
var pag spec.Pagination
// Detect limit param
for _, name := range []string{"limit", "maxresults", "pagesize", "page_size", "max_results", "per_page", "page[size]"} {
- if _, ok := paramNames[name]; ok {
- pag.LimitParam = name
+ if orig, ok := originalCase[name]; ok {
+ pag.LimitParam = orig
break
}
}
// Detect cursor param and pagination type
for _, name := range []string{"pagetoken", "page_token"} {
- if _, ok := paramNames[name]; ok {
- pag.CursorParam = name
+ if orig, ok := originalCase[name]; ok {
+ pag.CursorParam = orig
pag.Type = "page_token"
pag.NextCursorPath = "nextPageToken"
break
@@ -5060,8 +5063,8 @@ func detectPagination(params []spec.Param, op *openapi3.Operation) *spec.Paginat
}
if pag.Type == "" {
for _, name := range []string{"after", "cursor", "page[cursor]"} {
- if _, ok := paramNames[name]; ok {
- pag.CursorParam = name
+ if orig, ok := originalCase[name]; ok {
+ pag.CursorParam = orig
pag.Type = "cursor"
break
}
@@ -5069,8 +5072,8 @@ func detectPagination(params []spec.Param, op *openapi3.Operation) *spec.Paginat
}
if pag.Type == "" {
for _, name := range []string{"offset"} {
- if _, ok := paramNames[name]; ok {
- pag.CursorParam = name
+ if orig, ok := originalCase[name]; ok {
+ pag.CursorParam = orig
pag.Type = "offset"
break
}
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index 2feabd17..696d9639 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -6113,3 +6113,55 @@ paths:
"default captured verbatim; generator must use %q to escape at emit time")
})
}
+
+// TestDetectPaginationPreservesParameterCase guards #1353 — Google APIs
+// declare `pageSize` (camelCase) and reject the lowercased `pagesize`
+// the detector previously stored. The detector matches case-insensitively
+// but must store the parameter name as it appears in the spec.
+func TestDetectPaginationPreservesParameterCase(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ name string
+ paramName string
+ wantLimit string
+ }{
+ {"google camelCase pageSize", "pageSize", "pageSize"},
+ {"snake_case page_size", "page_size", "page_size"},
+ {"plain lowercase limit", "limit", "limit"},
+ {"mixed-case maxResults", "maxResults", "maxResults"},
+ {"per_page", "per_page", "per_page"},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ pag := detectPagination([]spec.Param{{Name: tc.paramName}}, nil)
+ require.NotNil(t, pag, "detector should classify %q as a paginator", tc.paramName)
+ assert.Equal(t, tc.wantLimit, pag.LimitParam)
+ })
+ }
+}
+
+func TestDetectPaginationPreservesCursorParamCase(t *testing.T) {
+ t.Parallel()
+
+ cases := []struct {
+ name string
+ paramName string
+ wantParam string
+ wantType string
+ }{
+ {"google camelCase pageToken", "pageToken", "pageToken", "page_token"},
+ {"snake_case page_token", "page_token", "page_token", "page_token"},
+ {"plain after", "after", "after", "cursor"},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ pag := detectPagination([]spec.Param{{Name: tc.paramName}}, nil)
+ require.NotNil(t, pag, "detector should classify %q as a cursor paginator", tc.paramName)
+ assert.Equal(t, tc.wantParam, pag.CursorParam)
+ assert.Equal(t, tc.wantType, pag.Type)
+ })
+ }
+}
← 01c816da fix(cli): pick gid/sid/uid before name in PK heuristic (#146
·
back to Cli Printing Press
·
fix(cli): research-dir state.json api_name overrides info.ti a9ffa08f →