Skip to content

Commit e5e1ec4

Browse files
author
yuguo.dtpe
committed
fix: preserve string-typed params as raw strings instead of parsing as JSON
1 parent cf025f4 commit e5e1ec4

3 files changed

Lines changed: 205 additions & 1 deletion

File tree

cmd/cmd_action.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"sort"
7+
"strconv"
78
"strings"
89

910
"github.com/spf13/cobra"
@@ -87,6 +88,7 @@ func doAction(ctx *Context, serviceName, action string) (err error) {
8788
method := "GET"
8889
contentType := ""
8990
apiInfo := rootSupport.GetApiInfo(serviceName, action)
91+
apiMeta := rootSupport.GetApiMeta(serviceName, action)
9092

9193
if apiInfo != nil && apiInfo.Method != "" {
9294
method = apiInfo.Method
@@ -100,7 +102,14 @@ func doAction(ctx *Context, serviceName, action string) (err error) {
100102
for _, f := range ctx.dynamicFlags.flags {
101103
// rebuild input
102104
if f.Name != "body" {
103-
if a, success := util.ParseToJsonArrayOrObject(strings.TrimSpace(f.value)); success {
105+
// Skip JSON parsing for parameters whose declared type is "string".
106+
// Without this, a value like '{"Statement":[...]}' is deserialized
107+
// into a Go map and then flattened into query params, which breaks
108+
// APIs that expect a raw JSON string (e.g. IAM CreatePolicy's
109+
// PolicyDocument parameter).
110+
if isStringParam(apiMeta, f.Name) {
111+
input[f.Name] = f.value
112+
} else if a, success := util.ParseToJsonArrayOrObject(strings.TrimSpace(f.value)); success {
104113
input[f.Name] = a
105114
} else {
106115
input[f.Name] = f.value
@@ -173,6 +182,69 @@ func doAction(ctx *Context, serviceName, action string) (err error) {
173182
return
174183
}
175184

185+
// isStringParam reports whether the named parameter should be treated as a
186+
// literal string when rebuilding request input.
187+
//
188+
// This includes both parameters declared as type "string" and indexed
189+
// elements of repeated string arrays whose metadata key ends with ".N" and is
190+
// declared as "array[string]" (for example ResourceNames.N -> --ResourceNames.0).
191+
// In those cases the caller must NOT attempt to parse the value as JSON.
192+
func isStringParam(apiMeta *ApiMeta, name string) bool {
193+
mt, matchedKey, ok := getRequestMetaType(apiMeta, name)
194+
if !ok {
195+
return false
196+
}
197+
198+
switch mt.TypeName {
199+
case "string":
200+
return true
201+
case "array[string]":
202+
return isIndexedStringArrayElement(matchedKey)
203+
default:
204+
return false
205+
}
206+
}
207+
208+
func isIndexedStringArrayElement(matchedKey string) bool {
209+
return strings.HasSuffix(matchedKey, ".N")
210+
}
211+
212+
func getRequestMetaType(apiMeta *ApiMeta, name string) (*MetaType, string, bool) {
213+
if apiMeta == nil || apiMeta.Request == nil || apiMeta.Request.MetaTypes == nil {
214+
return nil, "", false
215+
}
216+
217+
if mt, ok := apiMeta.Request.MetaTypes[name]; ok {
218+
return mt, name, true
219+
}
220+
221+
normalizedName := normalizeMetaTypeKey(name)
222+
if normalizedName == name {
223+
return nil, "", false
224+
}
225+
226+
mt, ok := apiMeta.Request.MetaTypes[normalizedName]
227+
return mt, normalizedName, ok
228+
}
229+
230+
func normalizeMetaTypeKey(name string) string {
231+
parts := strings.Split(name, ".")
232+
changed := false
233+
234+
for i, part := range parts {
235+
if _, err := strconv.Atoi(part); err == nil {
236+
parts[i] = "N"
237+
changed = true
238+
}
239+
}
240+
241+
if !changed {
242+
return name
243+
}
244+
245+
return strings.Join(parts, ".")
246+
}
247+
176248
func actionUsageTemplate(params []string) string {
177249
sort.Strings(params)
178250

cmd/cmd_action_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestIsStringParam(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
apiMeta *ApiMeta
9+
param string
10+
expected bool
11+
}{
12+
{
13+
name: "nil apiMeta",
14+
apiMeta: nil,
15+
param: "PolicyDocument",
16+
expected: false,
17+
},
18+
{
19+
name: "string type param",
20+
apiMeta: &ApiMeta{
21+
Request: &Meta{
22+
MetaTypes: map[string]*MetaType{
23+
"PolicyDocument": {TypeName: "string"},
24+
"PolicyName": {TypeName: "string"},
25+
},
26+
},
27+
},
28+
param: "PolicyDocument",
29+
expected: true,
30+
},
31+
{
32+
name: "non-string type param",
33+
apiMeta: &ApiMeta{
34+
Request: &Meta{
35+
MetaTypes: map[string]*MetaType{
36+
"Filters": {TypeName: "object"},
37+
},
38+
},
39+
},
40+
param: "Filters",
41+
expected: false,
42+
},
43+
{
44+
name: "unknown param",
45+
apiMeta: &ApiMeta{
46+
Request: &Meta{
47+
MetaTypes: map[string]*MetaType{
48+
"PolicyName": {TypeName: "string"},
49+
},
50+
},
51+
},
52+
param: "Unknown",
53+
expected: false,
54+
},
55+
{
56+
name: "indexed repeated string param",
57+
apiMeta: &ApiMeta{
58+
Request: &Meta{
59+
MetaTypes: map[string]*MetaType{
60+
"TagFilters.N.Key": {TypeName: "string"},
61+
},
62+
},
63+
},
64+
param: "TagFilters.1.Key",
65+
expected: true,
66+
},
67+
{
68+
name: "indexed repeated string array element",
69+
apiMeta: &ApiMeta{
70+
Request: &Meta{
71+
MetaTypes: map[string]*MetaType{
72+
"TagFilters.N.Values.N": {TypeName: "array[string]"},
73+
},
74+
},
75+
},
76+
param: "TagFilters.1.Values.1",
77+
expected: true,
78+
},
79+
{
80+
name: "indexed root string array element",
81+
apiMeta: &ApiMeta{
82+
Request: &Meta{
83+
MetaTypes: map[string]*MetaType{
84+
"ResourceNames.N": {TypeName: "array[string]"},
85+
},
86+
},
87+
},
88+
param: "ResourceNames.0",
89+
expected: true,
90+
},
91+
{
92+
name: "root string array is not treated as string literal",
93+
apiMeta: &ApiMeta{
94+
Request: &Meta{
95+
MetaTypes: map[string]*MetaType{
96+
"ResourceNames": {TypeName: "array[string]"},
97+
},
98+
},
99+
},
100+
param: "ResourceNames",
101+
expected: false,
102+
},
103+
{
104+
name: "indexed repeated non-string param",
105+
apiMeta: &ApiMeta{
106+
Request: &Meta{
107+
MetaTypes: map[string]*MetaType{
108+
"TagFilters.N": {TypeName: "object"},
109+
},
110+
},
111+
},
112+
param: "TagFilters.1",
113+
expected: false,
114+
},
115+
}
116+
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
got := isStringParam(tt.apiMeta, tt.param)
120+
if got != tt.expected {
121+
t.Errorf("isStringParam(%v, %q) = %v, want %v", tt.apiMeta, tt.param, got, tt.expected)
122+
}
123+
})
124+
}
125+
}

cmd/root_support.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ func (r *RootSupport) GetVersion(svc string) string {
117117
return r.Versions[svc]
118118
}
119119

120+
func (r *RootSupport) GetApiMeta(svc string, action string) *ApiMeta {
121+
if metas, ok := r.SupportTypes[svc]; ok {
122+
return metas[action]
123+
}
124+
return nil
125+
}
126+
120127
func (r *RootSupport) GetApiInfo(svc string, action string) *ApiInfo {
121128
for k, v := range r.SupportAction {
122129
if k == svc {

0 commit comments

Comments
 (0)