Skip to content

Commit ea9c491

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

File tree

2 files changed

+131
-5
lines changed

2 files changed

+131
-5
lines changed

query.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package graphql
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
67
"io"
8+
"os"
79
"reflect"
810
"sort"
911

@@ -88,16 +90,23 @@ func writeArgumentType(w io.Writer, t reflect.Type, value bool) {
8890
// E.g., struct{Foo Int, BarBaz *Boolean} -> "{foo,barBaz}".
8991
func query(v interface{}) string {
9092
var buf bytes.Buffer
91-
writeQuery(&buf, reflect.TypeOf(v), false)
93+
writeQuery(&buf, reflect.TypeOf(v), reflect.ValueOf(v), false)
9294
return buf.String()
9395
}
9496

97+
var Debug = false
98+
9599
// writeQuery writes a minified query for t to w.
96100
// 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) {
101+
func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) {
102+
if Debug {
103+
_, _ = fmt.Fprintf(os.Stderr, "%v\n%v\n\n", t, TypeSafe(v))
104+
}
98105
switch t.Kind() {
99-
case reflect.Ptr, reflect.Slice:
100-
writeQuery(w, t.Elem(), false)
106+
case reflect.Ptr:
107+
writeQuery(w, t.Elem(), ElemSafe(v), false)
108+
case reflect.Slice:
109+
writeQuery(w, t.Elem(), IndexSafe(v, 0), false)
101110
case reflect.Struct:
102111
// If the type implements json.Unmarshaler, it's a scalar. Don't expand it.
103112
if reflect.PtrTo(t).Implements(jsonUnmarshaler) {
@@ -120,12 +129,51 @@ func writeQuery(w io.Writer, t reflect.Type, inline bool) {
120129
io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase())
121130
}
122131
}
123-
writeQuery(w, f.Type, inlineField)
132+
writeQuery(w, f.Type, FieldSafe(v, i), inlineField)
124133
}
125134
if !inline {
126135
io.WriteString(w, "}")
127136
}
137+
case reflect.Map: // handle map[string]interface{}
138+
it := v.MapRange()
139+
_, _ = io.WriteString(w, "{")
140+
for it.Next() {
141+
// it.Value() returns interface{}, so we need to use reflect.ValueOf
142+
// to cast it away
143+
key, val := it.Key(), reflect.ValueOf(it.Value().Interface())
144+
_, _ = io.WriteString(w, key.String())
145+
writeQuery(w, val.Type(), val, false)
146+
}
147+
_, _ = io.WriteString(w, "}")
148+
}
149+
}
150+
151+
func IndexSafe(v reflect.Value, i int) reflect.Value {
152+
if v.IsValid() && i < v.Len() {
153+
return v.Index(i)
154+
}
155+
return reflect.ValueOf(nil)
156+
}
157+
158+
func TypeSafe(v reflect.Value) reflect.Type {
159+
if v.IsValid() {
160+
return v.Type()
161+
}
162+
return reflect.TypeOf((interface{})(nil))
163+
}
164+
165+
func ElemSafe(v reflect.Value) reflect.Value {
166+
if v.IsValid() {
167+
return v.Elem()
168+
}
169+
return reflect.ValueOf(nil)
170+
}
171+
172+
func FieldSafe(valStruct reflect.Value, i int) reflect.Value {
173+
if valStruct.IsValid() {
174+
return valStruct.Field(i)
128175
}
176+
return reflect.ValueOf(nil)
129177
}
130178

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

query_test.go

Lines changed: 78 additions & 0 deletions
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)