From ea9c4915d77cfc29003c1e2d360223724be9c4ab Mon Sep 17 00:00:00 2001 From: Borodin Gregory Date: Wed, 10 Nov 2021 17:39:03 +0300 Subject: [PATCH] Allow map[string]interface{} instead struct in query and mutation #80 --- query.go | 58 ++++++++++++++++++++++++++++++++++---- query_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/query.go b/query.go index e10b771..c2ec013 100644 --- a/query.go +++ b/query.go @@ -3,7 +3,9 @@ package graphql import ( "bytes" "encoding/json" + "fmt" "io" + "os" "reflect" "sort" @@ -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) { @@ -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() diff --git a/query_test.go b/query_test.go index 4de8cb5..a06a73b 100644 --- a/query_test.go +++ b/query_test.go @@ -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{} { @@ -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{} @@ -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)