Skip to content

Commit 79562bc

Browse files
committed
Allow map[string]interface{} instead struct in query and mutation #80
1 parent 386dd16 commit 79562bc

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

query.go

+46-5
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,18 @@ func writeArgumentType(w io.Writer, t reflect.Type, value bool) {
8888
// E.g., struct{Foo Int, BarBaz *Boolean} -> "{foo,barBaz}".
8989
func query(v interface{}) string {
9090
var buf bytes.Buffer
91-
writeQuery(&buf, reflect.TypeOf(v), false)
91+
writeQuery(&buf, reflect.TypeOf(v), reflect.ValueOf(v), false)
9292
return buf.String()
9393
}
9494

9595
// writeQuery writes a minified query for t to w.
9696
// If inline is true, the struct fields of t are inlined into parent struct.
97-
func writeQuery(w io.Writer, t reflect.Type, inline bool) {
97+
func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) {
9898
switch t.Kind() {
99-
case reflect.Ptr, reflect.Slice:
100-
writeQuery(w, t.Elem(), false)
99+
case reflect.Ptr:
100+
writeQuery(w, t.Elem(), ElemSafe(v), false)
101+
case reflect.Slice:
102+
writeQuery(w, t.Elem(), IndexSafe(v, 0), false)
101103
case reflect.Struct:
102104
// If the type implements json.Unmarshaler, it's a scalar. Don't expand it.
103105
if reflect.PtrTo(t).Implements(jsonUnmarshaler) {
@@ -120,12 +122,51 @@ func writeQuery(w io.Writer, t reflect.Type, inline bool) {
120122
io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase())
121123
}
122124
}
123-
writeQuery(w, f.Type, inlineField)
125+
writeQuery(w, f.Type, FieldSafe(v, i), inlineField)
124126
}
125127
if !inline {
126128
io.WriteString(w, "}")
127129
}
130+
case reflect.Map: // handle map[string]interface{}
131+
it := v.MapRange()
132+
_, _ = io.WriteString(w, "{")
133+
for it.Next() {
134+
// it.Value() returns interface{}, so we need to use reflect.ValueOf
135+
// to cast it away
136+
key, val := it.Key(), reflect.ValueOf(it.Value().Interface())
137+
_, _ = io.WriteString(w, key.String())
138+
writeQuery(w, val.Type(), val, false)
139+
}
140+
_, _ = io.WriteString(w, "}")
141+
}
142+
}
143+
144+
func IndexSafe(v reflect.Value, i int) reflect.Value {
145+
if v.IsValid() && i < v.Len() {
146+
return v.Index(i)
147+
}
148+
return reflect.ValueOf(nil)
149+
}
150+
151+
func TypeSafe(v reflect.Value) reflect.Type {
152+
if v.IsValid() {
153+
return v.Type()
154+
}
155+
return reflect.TypeOf((interface{})(nil))
156+
}
157+
158+
func ElemSafe(v reflect.Value) reflect.Value {
159+
if v.IsValid() {
160+
return v.Elem()
161+
}
162+
return reflect.ValueOf(nil)
163+
}
164+
165+
func FieldSafe(valStruct reflect.Value, i int) reflect.Value {
166+
if valStruct.IsValid() {
167+
return valStruct.Field(i)
128168
}
169+
return reflect.ValueOf(nil)
129170
}
130171

131172
var jsonUnmarshaler = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()

query_test.go

+78
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,65 @@ func TestConstructQuery(t *testing.T) {
188188
},
189189
want: `query($issueNumber:Int!$repositoryName:String!$repositoryOwner:String!){repository(owner: $repositoryOwner, name: $repositoryName){issue(number: $issueNumber){reactionGroups{users(first:10){nodes{login}}}}}}`,
190190
},
191+
// check same thing with repository inner map work
192+
{
193+
inV: func() interface{} {
194+
type query struct {
195+
Repository map[string]interface{} `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
196+
}
197+
type issue struct {
198+
ReactionGroups []struct {
199+
Users struct {
200+
Nodes []struct {
201+
Login String
202+
}
203+
} `graphql:"users(first:10)"`
204+
}
205+
}
206+
return query{Repository: map[string]interface{}{
207+
"issue(number: $issueNumber)": issue{},
208+
}}
209+
}(),
210+
inVariables: map[string]interface{}{
211+
"repositoryOwner": String("shurcooL-test"),
212+
"repositoryName": String("test-repo"),
213+
"issueNumber": Int(1),
214+
},
215+
want: `query($issueNumber:Int!$repositoryName:String!$repositoryOwner:String!){repository(owner: $repositoryOwner, name: $repositoryName){issue(number: $issueNumber){reactionGroups{users(first:10){nodes{login}}}}}}`,
216+
},
217+
// check inner maps work inside slices
218+
{
219+
inV: func() interface{} {
220+
type query struct {
221+
Repository map[string]interface{} `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
222+
}
223+
type issue struct {
224+
ReactionGroups []struct {
225+
Users map[string]interface{} `graphql:"users(first:10)"`
226+
}
227+
}
228+
type nodes []struct {
229+
Login String
230+
}
231+
return query{Repository: map[string]interface{}{
232+
"issue(number: $issueNumber)": issue{
233+
ReactionGroups: []struct {
234+
Users map[string]interface{} `graphql:"users(first:10)"`
235+
}{
236+
{Users: map[string]interface{}{
237+
"nodes": nodes{},
238+
}},
239+
},
240+
},
241+
}}
242+
}(),
243+
inVariables: map[string]interface{}{
244+
"repositoryOwner": String("shurcooL-test"),
245+
"repositoryName": String("test-repo"),
246+
"issueNumber": Int(1),
247+
},
248+
want: `query($issueNumber:Int!$repositoryName:String!$repositoryOwner:String!){repository(owner: $repositoryOwner, name: $repositoryName){issue(number: $issueNumber){reactionGroups{users(first:10){nodes{login}}}}}}`,
249+
},
191250
// Embedded structs without graphql tag should be inlined in query.
192251
{
193252
inV: func() interface{} {
@@ -236,6 +295,14 @@ func TestConstructQuery(t *testing.T) {
236295
}
237296
}
238297

298+
type CreateUser struct {
299+
Login string
300+
}
301+
302+
type DeleteUser struct {
303+
Login string
304+
}
305+
239306
func TestConstructMutation(t *testing.T) {
240307
tests := []struct {
241308
inV interface{}
@@ -262,6 +329,17 @@ func TestConstructMutation(t *testing.T) {
262329
},
263330
want: `mutation($input:AddReactionInput!){addReaction(input:$input){subject{reactionGroups{users{totalCount}}}}}`,
264331
},
332+
{
333+
inV: map[string]interface{}{
334+
"createUser(login:$login1)": &CreateUser{},
335+
"deleteUser(login:$login2)": &DeleteUser{},
336+
},
337+
inVariables: map[string]interface{}{
338+
"login1": String("grihabor"),
339+
"login2": String("diman"),
340+
},
341+
want: "mutation($login1:String!$login2:String!){createUser(login:$login1){login}deleteUser(login:$login2){login}}",
342+
},
265343
}
266344
for _, tc := range tests {
267345
got := constructMutation(tc.inV, tc.inVariables)

0 commit comments

Comments
 (0)