Skip to content

Commit e582242

Browse files
authored
Merge pull request #282 from salman-ahmad/master
Use Struct Field Resolver instead of Method
2 parents d5b7dc6 + e3e3046 commit e582242

File tree

8 files changed

+438
-81
lines changed

8 files changed

+438
-81
lines changed

README.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,17 @@ $ curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query
6565

6666
### Resolvers
6767

68-
A resolver must have one method for each field of the GraphQL type it resolves. The method name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the field's name in a non-case-sensitive way.
68+
A resolver must have one method or field for each field of the GraphQL type it resolves. The method or field name has to be [exported](https://golang.org/ref/spec#Exported_identifiers) and match the schema's field's name in a non-case-sensitive way.
69+
You can use struct fields as resolvers by using `SchemaOpt: UseFieldResolvers()`. For example,
70+
```
71+
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers()}
72+
schema := graphql.MustParseSchema(s, &query{}, opts...)
73+
```
74+
75+
When using `UseFieldResolvers` schema option, a struct field will be used *only* when:
76+
- there is no method for a struct field
77+
- a struct field does not implement an interface method
78+
- a struct field does not have arguments
6979

7080
The method has up to two arguments:
7181

example/social/README.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
### Social App
2+
3+
A simple example of how to use struct fields as resolvers instead of methods.
4+
5+
To run this server
6+
7+
`go run ./example/field-resolvers/server/server.go`
8+
9+
and go to localhost:9011 to interact

example/social/server/server.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
7+
"github.com/graph-gophers/graphql-go"
8+
"github.com/graph-gophers/graphql-go/example/social"
9+
"github.com/graph-gophers/graphql-go/relay"
10+
)
11+
12+
func main() {
13+
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)}
14+
schema := graphql.MustParseSchema(social.Schema, &social.Resolver{}, opts...)
15+
16+
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
w.Write(page)
18+
}))
19+
20+
http.Handle("/query", &relay.Handler{Schema: schema})
21+
22+
log.Fatal(http.ListenAndServe(":9011", nil))
23+
}
24+
25+
var page = []byte(`
26+
<!DOCTYPE html>
27+
<html>
28+
<head>
29+
<link href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" rel="stylesheet" />
30+
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
31+
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
32+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
33+
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
34+
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
35+
</head>
36+
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
37+
<div id="graphiql" style="height: 100vh;">Loading...</div>
38+
<script>
39+
function graphQLFetcher(graphQLParams) {
40+
return fetch("/query", {
41+
method: "post",
42+
body: JSON.stringify(graphQLParams),
43+
credentials: "include",
44+
}).then(function (response) {
45+
return response.text();
46+
}).then(function (responseBody) {
47+
try {
48+
return JSON.parse(responseBody);
49+
} catch (error) {
50+
return responseBody;
51+
}
52+
});
53+
}
54+
55+
ReactDOM.render(
56+
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
57+
document.getElementById("graphiql")
58+
);
59+
</script>
60+
</body>
61+
</html>
62+
`)

example/social/social.go

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package social
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
"time"
9+
10+
"github.com/graph-gophers/graphql-go"
11+
)
12+
13+
const Schema = `
14+
schema {
15+
query: Query
16+
}
17+
18+
type Query {
19+
admin(id: ID!, role: Role = ADMIN): Admin!
20+
user(id: ID!): User!
21+
search(text: String!): [SearchResult]!
22+
}
23+
24+
interface Admin {
25+
id: ID!
26+
name: String!
27+
role: Role!
28+
}
29+
30+
scalar Time
31+
32+
type User implements Admin {
33+
id: ID!
34+
name: String!
35+
email: String!
36+
role: Role!
37+
phone: String!
38+
address: [String!]
39+
friends(page: Pagination): [User]
40+
createdAt: Time!
41+
}
42+
43+
input Pagination {
44+
first: Int
45+
last: Int
46+
}
47+
48+
enum Role {
49+
ADMIN
50+
USER
51+
}
52+
53+
union SearchResult = User
54+
`
55+
56+
type page struct {
57+
First *float64
58+
Last *float64
59+
}
60+
61+
type admin interface {
62+
ID() graphql.ID
63+
Name() string
64+
Role() string
65+
}
66+
67+
type searchResult struct {
68+
result interface{}
69+
}
70+
71+
func (r *searchResult) ToUser() (*user, bool) {
72+
res, ok := r.result.(*user)
73+
return res, ok
74+
}
75+
76+
type user struct {
77+
IDField string
78+
NameField string
79+
RoleField string
80+
Email string
81+
Phone string
82+
Address *[]string
83+
Friends *[]*user
84+
CreatedAt graphql.Time
85+
}
86+
87+
func (u user) ID() graphql.ID {
88+
return graphql.ID(u.IDField)
89+
}
90+
91+
func (u user) Name() string {
92+
return u.NameField
93+
}
94+
95+
func (u user) Role() string {
96+
return u.RoleField
97+
}
98+
99+
func (u user) FriendsResolver(args struct{ Page *page }) (*[]*user, error) {
100+
var from int
101+
numFriends := len(*u.Friends)
102+
to := numFriends
103+
104+
if args.Page != nil {
105+
if args.Page.First != nil {
106+
from = int(*args.Page.First)
107+
if from > numFriends {
108+
return nil, errors.New("not enough users")
109+
}
110+
}
111+
if args.Page.Last != nil {
112+
to = int(*args.Page.Last)
113+
if to == 0 || to > numFriends {
114+
to = numFriends
115+
}
116+
}
117+
}
118+
119+
friends := (*u.Friends)[from:to]
120+
121+
return &friends, nil
122+
}
123+
124+
var users = []*user{
125+
{
126+
IDField: "0x01",
127+
NameField: "Albus Dumbledore",
128+
RoleField: "ADMIN",
129+
Email: "Albus@hogwarts.com",
130+
Phone: "000-000-0000",
131+
Address: &[]string{"Office @ Hogwarts", "where Horcruxes are"},
132+
CreatedAt: graphql.Time{Time: time.Now()},
133+
},
134+
{
135+
IDField: "0x02",
136+
NameField: "Harry Potter",
137+
RoleField: "USER",
138+
Email: "harry@hogwarts.com",
139+
Phone: "000-000-0001",
140+
Address: &[]string{"123 dorm room @ Hogwarts", "456 random place"},
141+
CreatedAt: graphql.Time{Time: time.Now()},
142+
},
143+
{
144+
IDField: "0x03",
145+
NameField: "Hermione Granger",
146+
RoleField: "USER",
147+
Email: "hermione@hogwarts.com",
148+
Phone: "000-000-0011",
149+
Address: &[]string{"233 dorm room @ Hogwarts", "786 @ random place"},
150+
CreatedAt: graphql.Time{Time: time.Now()},
151+
},
152+
{
153+
IDField: "0x04",
154+
NameField: "Ronald Weasley",
155+
RoleField: "USER",
156+
Email: "ronald@hogwarts.com",
157+
Phone: "000-000-0111",
158+
Address: &[]string{"411 dorm room @ Hogwarts", "981 @ random place"},
159+
CreatedAt: graphql.Time{Time: time.Now()},
160+
},
161+
}
162+
163+
var usersMap = make(map[string]*user)
164+
165+
func init() {
166+
users[0].Friends = &[]*user{users[1]}
167+
users[1].Friends = &[]*user{users[0], users[2], users[3]}
168+
users[2].Friends = &[]*user{users[1], users[3]}
169+
users[3].Friends = &[]*user{users[1], users[2]}
170+
for _, usr := range users {
171+
usersMap[usr.IDField] = usr
172+
}
173+
}
174+
175+
type Resolver struct{}
176+
177+
func (r *Resolver) Admin(ctx context.Context, args struct {
178+
ID string
179+
Role string
180+
}) (admin, error) {
181+
if usr, ok := usersMap[args.ID]; ok {
182+
if usr.RoleField == args.Role {
183+
return *usr, nil
184+
}
185+
}
186+
err := fmt.Errorf("user with id=%s and role=%s does not exist", args.ID, args.Role)
187+
return user{}, err
188+
}
189+
190+
func (r *Resolver) User(ctx context.Context, args struct{ Id string }) (user, error) {
191+
if usr, ok := usersMap[args.Id]; ok {
192+
return *usr, nil
193+
}
194+
err := fmt.Errorf("user with id=%s does not exist", args.Id)
195+
return user{}, err
196+
}
197+
198+
func (r *Resolver) Search(ctx context.Context, args struct{ Text string }) ([]*searchResult, error) {
199+
var result []*searchResult
200+
for _, usr := range users {
201+
if strings.Contains(usr.NameField, args.Text) {
202+
result = append(result, &searchResult{usr})
203+
}
204+
}
205+
return result, nil
206+
}

graphql.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ package graphql
22

33
import (
44
"context"
5-
"fmt"
6-
75
"encoding/json"
6+
"fmt"
87

98
"github.com/graph-gophers/graphql-go/errors"
109
"github.com/graph-gophers/graphql-go/internal/common"
@@ -84,6 +83,13 @@ func UseStringDescriptions() SchemaOpt {
8483
}
8584
}
8685

86+
// UseFieldResolvers specifies whether to use struct field resolvers
87+
func UseFieldResolvers() SchemaOpt {
88+
return func(s *Schema) {
89+
s.schema.UseFieldResolvers = true
90+
}
91+
}
92+
8793
// MaxDepth specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking.
8894
func MaxDepth(n int) SchemaOpt {
8995
return func(s *Schema) {

internal/exec/exec.go

+26-17
Original file line numberDiff line numberDiff line change
@@ -178,24 +178,33 @@ func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *p
178178
return errors.Errorf("%s", err) // don't execute any more resolvers if context got cancelled
179179
}
180180

181-
var in []reflect.Value
182-
if f.field.HasContext {
183-
in = append(in, reflect.ValueOf(traceCtx))
184-
}
185-
if f.field.ArgsPacker != nil {
186-
in = append(in, f.field.PackedArgs)
187-
}
188-
callOut := f.resolver.Method(f.field.MethodIndex).Call(in)
189-
result = callOut[0]
190-
if f.field.HasError && !callOut[1].IsNil() {
191-
resolverErr := callOut[1].Interface().(error)
192-
err := errors.Errorf("%s", resolverErr)
193-
err.Path = path.toSlice()
194-
err.ResolverError = resolverErr
195-
if ex, ok := callOut[1].Interface().(extensionser); ok {
196-
err.Extensions = ex.Extensions()
181+
res := f.resolver
182+
if f.field.UseMethodResolver() {
183+
var in []reflect.Value
184+
if f.field.HasContext {
185+
in = append(in, reflect.ValueOf(traceCtx))
186+
}
187+
if f.field.ArgsPacker != nil {
188+
in = append(in, f.field.PackedArgs)
189+
}
190+
callOut := res.Method(f.field.MethodIndex).Call(in)
191+
result = callOut[0]
192+
if f.field.HasError && !callOut[1].IsNil() {
193+
resolverErr := callOut[1].Interface().(error)
194+
err := errors.Errorf("%s", resolverErr)
195+
err.Path = path.toSlice()
196+
err.ResolverError = resolverErr
197+
if ex, ok := callOut[1].Interface().(extensionser); ok {
198+
err.Extensions = ex.Extensions()
199+
}
200+
return err
201+
}
202+
} else {
203+
// TODO extract out unwrapping ptr logic to a common place
204+
if res.Kind() == reflect.Ptr {
205+
res = res.Elem()
197206
}
198-
return err
207+
result = res.Field(f.field.FieldIndex)
199208
}
200209
return nil
201210
}()

0 commit comments

Comments
 (0)