diff --git a/README.md b/README.md index 3353795..a84a5f4 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,44 @@ fmt.Printf("Created a %v star review: %v\n", m.CreateReview.Stars, m.CreateRevie // Created a 5 star review: This is a great movie! ``` +### Multiple mutations with ordered map + +You might need to make multiple mutations in single query. It's not very convenient with structs +so you can use ordered map `[][2]interface{}` instead. + +For example, to make the following GraphQL mutation: + +```GraphQL +mutation($login1: String!, $login2: String!, $login3: String!) { + createUser(login: $login1) { login } + createUser(login: $login2) { login } + createUser(login: $login3) { login } +} +variables { + "login1": "grihabor", + "login2": "diman", + "login3": "indigo" +} +``` + +You can define: + +```Go +type CreateUser struct { + Login graphql.String +} +m := [][2]interface{}{ + {"createUser(login: $login1)", &CreateUser{}}, + {"createUser(login: $login2)", &CreateUser{}}, + {"createUser(login: $login3)", &CreateUser{}}, +} +variables := map[string]interface{}{ + "login1": graphql.String("grihabor"), + "login2": graphql.String("diman"), + "login3": graphql.String("indigo"), +} +``` + Directories ----------- diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db48660 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/shurcooL/graphql + +go 1.16 + +require ( + github.com/graph-gophers/graphql-go v1.2.0 + github.com/stretchr/testify v1.7.0 // indirect + golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a046f14 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/graph-gophers/graphql-go v1.2.0 h1:j3tCG0UcE+3f84OAw/4/6YQKyTr+r0yuUKtnxiu5OH4= +github.com/graph-gophers/graphql-go v1.2.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/net v0.0.0-20211109214657-ef0fda0de508 h1:v3NKo+t/Kc3EASxaKZ82lwK6mCf4ZeObQBduYFZHo7c= +golang.org/x/net v0.0.0-20211109214657-ef0fda0de508/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/jsonutil/graphql.go b/internal/jsonutil/graphql.go index 15bae24..97dda33 100644 --- a/internal/jsonutil/graphql.go +++ b/internal/jsonutil/graphql.go @@ -53,7 +53,17 @@ type decoder struct { // The reason there's more than one stack is because we might be unmarshaling // a single JSON value into multiple GraphQL fragments or embedded structs, so // we keep track of them all. - vs [][]reflect.Value + vs []stack +} + +type stack []reflect.Value + +func (s stack) Top() reflect.Value { + return s[len(s)-1] +} + +func (s stack) Pop() stack { + return s[:len(s)-1] } // Decode decodes a single JSON value from d.tokenizer into v. @@ -62,7 +72,7 @@ func (d *decoder) Decode(v interface{}) error { if rv.Kind() != reflect.Ptr { return fmt.Errorf("cannot decode into non-pointer %T", v) } - d.vs = [][]reflect.Value{{rv.Elem()}} + d.vs = []stack{{rv.Elem()}} return d.decode() } @@ -88,16 +98,22 @@ func (d *decoder) decode() error { } someFieldExist := false for i := range d.vs { - v := d.vs[i][len(d.vs[i])-1] - if v.Kind() == reflect.Ptr { + v := d.vs[i].Top() + if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface { v = v.Elem() } var f reflect.Value - if v.Kind() == reflect.Struct { + switch v.Kind() { + case reflect.Struct: f = fieldByGraphQLName(v, key) if f.IsValid() { someFieldExist = true } + case reflect.Slice: + f = orderedMapValueByGraphQLName(v, key) + if f.IsValid() { + someFieldExist = true + } } d.vs[i] = append(d.vs[i], f) } @@ -118,13 +134,15 @@ func (d *decoder) decode() error { case d.state() == '[' && tok != json.Delim(']'): someSliceExist := false for i := range d.vs { - v := d.vs[i][len(d.vs[i])-1] + v := d.vs[i].Top() if v.Kind() == reflect.Ptr { v = v.Elem() } var f reflect.Value if v.Kind() == reflect.Slice { - v.Set(reflect.Append(v, reflect.Zero(v.Type().Elem()))) // v = append(v, T). + // we want to append the template item copy + // so that all the inner structure gets preserved + v.Set(reflect.Append(v, copyTemplate(v.Index(0)))) // v = append(v, T). f = v.Index(v.Len() - 1) someSliceExist = true } @@ -140,7 +158,7 @@ func (d *decoder) decode() error { // Value. for i := range d.vs { - v := d.vs[i][len(d.vs[i])-1] + v := d.vs[i].Top() if !v.IsValid() { continue } @@ -160,7 +178,7 @@ func (d *decoder) decode() error { frontier := make([]reflect.Value, len(d.vs)) // Places to look for GraphQL fragments/embedded structs. for i := range d.vs { - v := d.vs[i][len(d.vs[i])-1] + v := d.vs[i].Top() frontier[i] = v // TODO: Do this recursively or not? Add a test case if needed. if v.Kind() == reflect.Ptr && v.IsNil() { @@ -175,14 +193,23 @@ func (d *decoder) decode() error { if v.Kind() == reflect.Ptr { v = v.Elem() } - if v.Kind() != reflect.Struct { - continue - } - for i := 0; i < v.NumField(); i++ { - if isGraphQLFragment(v.Type().Field(i)) || v.Type().Field(i).Anonymous { - // Add GraphQL fragment or embedded struct. - d.vs = append(d.vs, []reflect.Value{v.Field(i)}) - frontier = append(frontier, v.Field(i)) + if v.Kind() == reflect.Struct { + for i := 0; i < v.NumField(); i++ { + if isGraphQLFragment(v.Type().Field(i)) || v.Type().Field(i).Anonymous { + // Add GraphQL fragment or embedded struct. + d.vs = append(d.vs, []reflect.Value{v.Field(i)}) + frontier = append(frontier, v.Field(i)) + } + } + } else if isOrderedMap(v) { + for i := 0; i < v.Len(); i++ { + pair := v.Index(i) + key, val := pair.Index(0), pair.Index(1) + if keyForGraphQLFragment(key.Interface().(string)) { + // Add GraphQL fragment or embedded struct. + d.vs = append(d.vs, []reflect.Value{val}) + frontier = append(frontier, val) + } } } } @@ -192,7 +219,7 @@ func (d *decoder) decode() error { d.pushState(tok) for i := range d.vs { - v := d.vs[i][len(d.vs[i])-1] + v := d.vs[i].Top() // TODO: Confirm this is needed, write a test case. //if v.Kind() == reflect.Ptr && v.IsNil() { // v.Set(reflect.New(v.Type().Elem())) // v = new(T). @@ -205,10 +232,28 @@ func (d *decoder) decode() error { if v.Kind() != reflect.Slice { continue } - v.Set(reflect.MakeSlice(v.Type(), 0, 0)) // v = make(T, 0, 0). + newSlice := reflect.MakeSlice(v.Type(), 0, 0) // v = make(T, 0, 0). + switch v.Len() { + case 0: + // if there is no template we need to create one so that we can + // handle both cases (with or without a template) in the same way + newSlice = reflect.Append(newSlice, reflect.Zero(v.Type().Elem())) + case 1: + // if there is a template, we need to keep it at index 0 + newSlice = reflect.Append(newSlice, v.Index(0)) + case 2: + msg := fmt.Sprintf("template slice can only have 1 item") + panic(msg) + } + v.Set(newSlice) } - case '}', ']': - // End of object or array. + case '}': + // End of object. + d.popAllVs() + d.popState() + case ']': + // End of array. + d.popLeftArrayTemplates() d.popAllVs() d.popState() default: @@ -221,6 +266,38 @@ func (d *decoder) decode() error { return nil } +func copyTemplate(template reflect.Value) reflect.Value { + if isOrderedMap(template) { + // copy slice if it's actually an ordered map + return copyOrderedMap(template) + } + if template.Kind() == reflect.Map { + msg := fmt.Sprintf("unsupported template type `%v`, use [][2]interface{} for ordered map instead", template.Type()) + panic(msg) + } + // don't need to copy regular slice + return template +} + +func isOrderedMap(v reflect.Value) bool { + if !v.IsValid() { + return false + } + t := v.Type() + return t.Kind() == reflect.Slice && + t.Elem().Kind() == reflect.Array && + t.Elem().Len() == 2 +} + +func copyOrderedMap(m reflect.Value) reflect.Value { + newMap := reflect.MakeSlice(m.Type(), 0, m.Len()) + for i := 0; i < m.Len(); i++ { + pair := m.Index(i) + newMap = reflect.Append(newMap, pair) + } + return newMap +} + // pushState pushes a new parse state s onto the stack. func (d *decoder) pushState(s json.Delim) { d.parseState = append(d.parseState, s) @@ -242,9 +319,9 @@ func (d *decoder) state() json.Delim { // popAllVs pops from all d.vs stacks, keeping only non-empty ones. func (d *decoder) popAllVs() { - var nonEmpty [][]reflect.Value + var nonEmpty []stack for i := range d.vs { - d.vs[i] = d.vs[i][:len(d.vs[i])-1] + d.vs[i] = d.vs[i].Pop() if len(d.vs[i]) > 0 { nonEmpty = append(nonEmpty, d.vs[i]) } @@ -252,6 +329,16 @@ func (d *decoder) popAllVs() { d.vs = nonEmpty } +// popLeftArrayTemplates pops left from last array items of all d.vs stacks. +func (d *decoder) popLeftArrayTemplates() { + for i := range d.vs { + v := d.vs[i].Top() + if v.IsValid() { + v.Set(v.Slice(1, v.Len())) + } + } +} + // fieldByGraphQLName returns an exported struct field of struct v // that matches GraphQL name, or invalid reflect.Value if none found. func fieldByGraphQLName(v reflect.Value, name string) reflect.Value { @@ -267,6 +354,19 @@ func fieldByGraphQLName(v reflect.Value, name string) reflect.Value { return reflect.Value{} } +// orderedMapValueByGraphQLName takes [][2]string, interprets it as an ordered map +// and returns value for corresponding key, or invalid reflect.Value if none found. +func orderedMapValueByGraphQLName(v reflect.Value, name string) reflect.Value { + for i := 0; i < v.Len(); i++ { + pair := v.Index(i) + key := pair.Index(0).Interface().(string) + if keyHasGraphQLName(key, name) { + return pair.Index(1) + } + } + return reflect.Value{} +} + // hasGraphQLName reports whether struct field f has GraphQL name. func hasGraphQLName(f reflect.StructField, name string) bool { value, ok := f.Tag.Lookup("graphql") @@ -275,6 +375,10 @@ func hasGraphQLName(f reflect.StructField, name string) bool { //return caseconv.MixedCapsToLowerCamelCase(f.Name) == name return strings.EqualFold(f.Name, name) } + return keyHasGraphQLName(value, name) +} + +func keyHasGraphQLName(value, name string) bool { value = strings.TrimSpace(value) // TODO: Parse better. if strings.HasPrefix(value, "...") { // GraphQL fragment. It doesn't have a name. @@ -295,6 +399,11 @@ func isGraphQLFragment(f reflect.StructField) bool { if !ok { return false } + return keyForGraphQLFragment(value) +} + +// isGraphQLFragment reports whether ordered map kv pair f is a GraphQL fragment. +func keyForGraphQLFragment(value string) bool { value = strings.TrimSpace(value) // TODO: Parse better. return strings.HasPrefix(value, "...") } @@ -307,5 +416,18 @@ func unmarshalValue(value json.Token, v reflect.Value) error { if err != nil { return err } - return json.Unmarshal(b, v.Addr().Interface()) + ty := v.Type() + if ty.Kind() == reflect.Interface { + if !v.Elem().IsValid() { + return json.Unmarshal(b, v.Addr().Interface()) + } + ty = v.Elem().Type() + } + newVal := reflect.New(ty) + err = json.Unmarshal(b, newVal.Interface()) + if err != nil { + return err + } + v.Set(newVal.Elem()) + return nil } diff --git a/internal/jsonutil/graphql_test.go b/internal/jsonutil/graphql_test.go index 6329ed8..cfeaafc 100644 --- a/internal/jsonutil/graphql_test.go +++ b/internal/jsonutil/graphql_test.go @@ -80,6 +80,25 @@ func TestUnmarshalGraphQL_jsonTag(t *testing.T) { } } +func TestUnmarshalGraphQL_orderedMap(t *testing.T) { + type query [][2]interface{} + got := query{ + {"foo", graphql.String("")}, + } + err := jsonutil.UnmarshalGraphQL([]byte(`{ + "foo": "bar" + }`), &got) + if err != nil { + t.Fatal(err) + } + want := query{ + {"foo", graphql.String("bar")}, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("not equal: %v != %v", got, want) + } +} + func TestUnmarshalGraphQL_array(t *testing.T) { type query struct { Foo []graphql.String @@ -104,7 +123,7 @@ func TestUnmarshalGraphQL_array(t *testing.T) { Baz: []graphql.String(nil), } if !reflect.DeepEqual(got, want) { - t.Error("not equal") + t.Errorf("not equal: %v != %v", got, want) } } @@ -149,6 +168,35 @@ func TestUnmarshalGraphQL_objectArray(t *testing.T) { } } +func TestUnmarshalGraphQL_orderedMapArray(t *testing.T) { + type query struct { + Foo [][][2]interface{} + } + got := query{ + Foo: [][][2]interface{}{ + {{"name", graphql.String("")}}, + }, + } + err := jsonutil.UnmarshalGraphQL([]byte(`{ + "foo": [ + {"name": "bar"}, + {"name": "baz"} + ] + }`), &got) + if err != nil { + t.Fatal(err) + } + want := query{ + Foo: [][][2]interface{}{ + {{"name", graphql.String("bar")}}, + {{"name", graphql.String("baz")}}, + }, + } + if !reflect.DeepEqual(got, want) { + t.Error("not equal") + } +} + func TestUnmarshalGraphQL_pointer(t *testing.T) { type query struct { Foo *graphql.String @@ -201,6 +249,37 @@ func TestUnmarshalGraphQL_objectPointerArray(t *testing.T) { } } +func TestUnmarshalGraphQL_orderedMapNullInArray(t *testing.T) { + type query struct { + Foo [][][2]interface{} + } + got := query{ + Foo: [][][2]interface{}{ + {{"name", ""}}, + }, + } + err := jsonutil.UnmarshalGraphQL([]byte(`{ + "foo": [ + {"name": "bar"}, + null, + {"name": "baz"} + ] + }`), &got) + if err != nil { + t.Fatal(err) + } + want := query{ + Foo: [][][2]interface{}{ + {{"name", "bar"}}, + nil, + {{"name", "baz"}}, + }, + } + if !reflect.DeepEqual(got, want) { + t.Error("not equal") + } +} + func TestUnmarshalGraphQL_pointerWithInlineFragment(t *testing.T) { type actor struct { User struct { @@ -267,6 +346,18 @@ func TestUnmarshalGraphQL_multipleValues(t *testing.T) { } } +func TestUnmarshalGraphQL_multipleValuesInOrderedMap(t *testing.T) { + type query [][2]interface{} + q := query{{"foo", ""}} + err := jsonutil.UnmarshalGraphQL([]byte(`{"foo": "bar"}{"foo": "baz"}`), &q) + if err == nil { + t.Fatal("got error: nil, want: non-nil") + } + if got, want := err.Error(), "invalid token '{' after top-level value"; got != want { + t.Errorf("got error: %v, want: %v", got, want) + } +} + func TestUnmarshalGraphQL_union(t *testing.T) { /* { @@ -326,6 +417,56 @@ func TestUnmarshalGraphQL_union(t *testing.T) { } } +func TestUnmarshalGraphQL_orderedMapUnion(t *testing.T) { + /* + { + __typename + ... on ClosedEvent { + createdAt + actor {login} + } + ... on ReopenedEvent { + createdAt + actor {login} + } + } + */ + actor := [][2]interface{}{{"login", ""}} + closedEvent := [][2]interface{}{{"actor", actor}, {"createdAt", time.Time{}}} + reopenedEvent := [][2]interface{}{{"actor", actor}, {"createdAt", time.Time{}}} + got := [][2]interface{}{ + {"__typename", ""}, + {"... on ClosedEvent", closedEvent}, + {"... on ReopenedEvent", reopenedEvent}, + } + err := jsonutil.UnmarshalGraphQL([]byte(`{ + "__typename": "ClosedEvent", + "createdAt": "2017-06-29T04:12:01Z", + "actor": { + "login": "shurcooL-test" + } + }`), &got) + if err != nil { + t.Fatal(err) + } + want := [][2]interface{}{ + {"__typename", "ClosedEvent"}, + {"... on ClosedEvent", [][2]interface{}{ + {"actor", [][2]interface{}{{"login", "shurcooL-test"}}}, + {"createdAt", time.Unix(1498709521, 0).UTC()}, + }}, + {"... on ReopenedEvent", [][2]interface{}{ + {"actor", [][2]interface{}{{"login", "shurcooL-test"}}}, + {"createdAt", time.Unix(1498709521, 0).UTC()}, + }}, + } + if !reflect.DeepEqual(got, want) { + t.Errorf("not equal:\ngot: %v\nwant: %v", got, want) + createdAt := got[1][1].([][2]interface{})[1] + t.Logf("key: %s, type: %v", createdAt[0], reflect.TypeOf(createdAt[1])) + } +} + // Issue https://github.com/shurcooL/githubv4/issues/18. func TestUnmarshalGraphQL_arrayInsideInlineFragment(t *testing.T) { /* diff --git a/query.go b/query.go index e10b771..68d4d13 100644 --- a/query.go +++ b/query.go @@ -3,6 +3,7 @@ package graphql import ( "bytes" "encoding/json" + "fmt" "io" "reflect" "sort" @@ -88,16 +89,16 @@ 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() } // 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) { 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.Struct: // If the type implements json.Unmarshaler, it's a scalar. Don't expand it. if reflect.PtrTo(t).Implements(jsonUnmarshaler) { @@ -120,12 +121,57 @@ 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.Slice: + if t.Elem().Kind() != reflect.Array { + writeQuery(w, t.Elem(), IndexSafe(v, 0), false) + return + } + // handle [][2]interface{} like an ordered map + if t.Elem().Len() != 2 { + err := fmt.Errorf("only arrays of len 2 are supported, got %v", t.Elem()) + panic(err.Error()) + } + sliceOfPairs := v + _, _ = io.WriteString(w, "{") + for i := 0; i < sliceOfPairs.Len(); i++ { + pair := sliceOfPairs.Index(i) + // it.Value() returns interface{}, so we need to use reflect.ValueOf + // to cast it away + key, val := pair.Index(0), reflect.ValueOf(pair.Index(1).Interface()) + _, _ = io.WriteString(w, key.Interface().(string)) + writeQuery(w, val.Type(), val, false) + } + _, _ = io.WriteString(w, "}") + case reflect.Map: + err := fmt.Errorf("type %v is not supported, use [][2]interface{} instead", t) + panic(err.Error()) + } +} + +func IndexSafe(v reflect.Value, i int) reflect.Value { + if v.IsValid() && i < v.Len() { + return v.Index(i) + } + return reflect.ValueOf(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..22d2f51 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 test above works with repository inner map + { + inV: func() interface{} { + type query struct { + Repository [][2]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: [][2]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 [][2]interface{} `graphql:"repository(owner: $repositoryOwner, name: $repositoryName)"` + } + type issue struct { + ReactionGroups []struct { + Users [][2]interface{} `graphql:"users(first:10)"` + } + } + type nodes []struct { + Login String + } + return query{Repository: [][2]interface{}{ + {"issue(number: $issueNumber)", issue{ + ReactionGroups: []struct { + Users [][2]interface{} `graphql:"users(first:10)"` + }{ + {Users: [][2]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: [][2]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)