Skip to content

Commit

Permalink
Allow map[string]interface{} instead struct in query and mutation shu…
Browse files Browse the repository at this point in the history
  • Loading branch information
grihabor committed Nov 10, 2021
1 parent 386dd16 commit ea9c491
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 5 deletions.
58 changes: 53 additions & 5 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package graphql
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"reflect"
"sort"

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

var Debug = false

// writeQuery writes a minified query for t to w.
// If inline is true, the struct fields of t are inlined into parent struct.
func writeQuery(w io.Writer, t reflect.Type, inline bool) {
func writeQuery(w io.Writer, t reflect.Type, v reflect.Value, inline bool) {
if Debug {
_, _ = fmt.Fprintf(os.Stderr, "%v\n%v\n\n", t, TypeSafe(v))
}
switch t.Kind() {
case reflect.Ptr, reflect.Slice:
writeQuery(w, t.Elem(), false)
case reflect.Ptr:
writeQuery(w, t.Elem(), ElemSafe(v), false)
case reflect.Slice:
writeQuery(w, t.Elem(), IndexSafe(v, 0), false)
case reflect.Struct:
// If the type implements json.Unmarshaler, it's a scalar. Don't expand it.
if reflect.PtrTo(t).Implements(jsonUnmarshaler) {
Expand All @@ -120,12 +129,51 @@ func writeQuery(w io.Writer, t reflect.Type, inline bool) {
io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase())
}
}
writeQuery(w, f.Type, inlineField)
writeQuery(w, f.Type, FieldSafe(v, i), inlineField)
}
if !inline {
io.WriteString(w, "}")
}
case reflect.Map: // handle map[string]interface{}
it := v.MapRange()
_, _ = io.WriteString(w, "{")
for it.Next() {
// it.Value() returns interface{}, so we need to use reflect.ValueOf
// to cast it away
key, val := it.Key(), reflect.ValueOf(it.Value().Interface())
_, _ = io.WriteString(w, key.String())
writeQuery(w, val.Type(), val, false)
}
_, _ = io.WriteString(w, "}")
}
}

func IndexSafe(v reflect.Value, i int) reflect.Value {
if v.IsValid() && i < v.Len() {
return v.Index(i)
}
return reflect.ValueOf(nil)
}

func TypeSafe(v reflect.Value) reflect.Type {
if v.IsValid() {
return v.Type()
}
return reflect.TypeOf((interface{})(nil))
}

func ElemSafe(v reflect.Value) reflect.Value {
if v.IsValid() {
return v.Elem()
}
return reflect.ValueOf(nil)
}

func FieldSafe(valStruct reflect.Value, i int) reflect.Value {
if valStruct.IsValid() {
return valStruct.Field(i)
}
return reflect.ValueOf(nil)
}

var jsonUnmarshaler = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
78 changes: 78 additions & 0 deletions query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,65 @@ func TestConstructQuery(t *testing.T) {
},
want: `query($issueNumber:Int!$repositoryName:String!$repositoryOwner:String!){repository(owner: $repositoryOwner, name: $repositoryName){issue(number: $issueNumber){reactionGroups{users(first:10){nodes{login}}}}}}`,
},
// check same thing with repository inner map work
{
inV: func() interface{} {
type query struct {
Repository map[string]interface{} `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
}
type issue struct {
ReactionGroups []struct {
Users struct {
Nodes []struct {
Login String
}
} `graphql:"users(first:10)"`
}
}
return query{Repository: map[string]interface{}{
"issue(number: $issueNumber)": issue{},
}}
}(),
inVariables: map[string]interface{}{
"repositoryOwner": String("shurcooL-test"),
"repositoryName": String("test-repo"),
"issueNumber": Int(1),
},
want: `query($issueNumber:Int!$repositoryName:String!$repositoryOwner:String!){repository(owner: $repositoryOwner, name: $repositoryName){issue(number: $issueNumber){reactionGroups{users(first:10){nodes{login}}}}}}`,
},
// check inner maps work inside slices
{
inV: func() interface{} {
type query struct {
Repository map[string]interface{} `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"`
}
type issue struct {
ReactionGroups []struct {
Users map[string]interface{} `graphql:"users(first:10)"`
}
}
type nodes []struct {
Login String
}
return query{Repository: map[string]interface{}{
"issue(number: $issueNumber)": issue{
ReactionGroups: []struct {
Users map[string]interface{} `graphql:"users(first:10)"`
}{
{Users: map[string]interface{}{
"nodes": nodes{},
}},
},
},
}}
}(),
inVariables: map[string]interface{}{
"repositoryOwner": String("shurcooL-test"),
"repositoryName": String("test-repo"),
"issueNumber": Int(1),
},
want: `query($issueNumber:Int!$repositoryName:String!$repositoryOwner:String!){repository(owner: $repositoryOwner, name: $repositoryName){issue(number: $issueNumber){reactionGroups{users(first:10){nodes{login}}}}}}`,
},
// Embedded structs without graphql tag should be inlined in query.
{
inV: func() interface{} {
Expand Down Expand Up @@ -236,6 +295,14 @@ func TestConstructQuery(t *testing.T) {
}
}

type CreateUser struct {
Login string
}

type DeleteUser struct {
Login string
}

func TestConstructMutation(t *testing.T) {
tests := []struct {
inV interface{}
Expand All @@ -262,6 +329,17 @@ func TestConstructMutation(t *testing.T) {
},
want: `mutation($input:AddReactionInput!){addReaction(input:$input){subject{reactionGroups{users{totalCount}}}}}`,
},
{
inV: map[string]interface{}{
"createUser(login:$login1)": &CreateUser{},
"deleteUser(login:$login2)": &DeleteUser{},
},
inVariables: map[string]interface{}{
"login1": String("grihabor"),
"login2": String("diman"),
},
want: "mutation($login1:String!$login2:String!){createUser(login:$login1){login}deleteUser(login:$login2){login}}",
},
}
for _, tc := range tests {
got := constructMutation(tc.inV, tc.inVariables)
Expand Down

0 comments on commit ea9c491

Please sign in to comment.