Skip to content

Use Struct Field Resolver instead of Method #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/internal/validation/testdata/graphql-js
/internal/validation/testdata/node_modules
/vendor
.DS_Store
.idea/
.vscode/
9 changes: 9 additions & 0 deletions example/social/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
### Social App

A simple example of how to use struct fields as resolvers instead of methods.

To run this server

`go ./example/field-resolvers/server/server.go`

and go to localhost:9011 to interact
66 changes: 66 additions & 0 deletions example/social/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"log"
"net/http"

"github.com/graph-gophers/graphql-go"
"github.com/graph-gophers/graphql-go/example/social"
"github.com/graph-gophers/graphql-go/relay"
)

var schema *graphql.Schema

func init() {
opts := []graphql.SchemaOpt{graphql.UseFieldResolvers(), graphql.MaxParallelism(20)}
schema = graphql.MustParseSchema(social.Schema, &social.Resolver{}, opts...)
}

func main() {
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(page)
}))

http.Handle("/query", &relay.Handler{Schema: schema})

log.Fatal(http.ListenAndServe(":9011", nil))
}

var page = []byte(`
<!DOCTYPE html>
<html>
<head>
<link href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
<div id="graphiql" style="height: 100vh;">Loading...</div>
<script>
function graphQLFetcher(graphQLParams) {
return fetch("/query", {
method: "post",
body: JSON.stringify(graphQLParams),
credentials: "include",
}).then(function (response) {
return response.text();
}).then(function (responseBody) {
try {
return JSON.parse(responseBody);
} catch (error) {
return responseBody;
}
});
}

ReactDOM.render(
React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
document.getElementById("graphiql")
);
</script>
</body>
</html>
`)
169 changes: 169 additions & 0 deletions example/social/social.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package social

import (
"context"
"fmt"
)

const Schema = `
schema {
query: Query
}

type Query {
admin(id: ID!, role: Role = ADMIN): Admin!
user(id: ID!): User!
}

interface Admin {
id: ID!
name: String!
role: Role!
}

type User implements Admin {
id: ID!
name: String!
email: String!
role: Role!
phone: String!
address: [String!]
friends(page: Pagination): [User]
}

input Pagination {
first: Int
last: Int
}

enum Role {
ADMIN
USER
}
`

type page struct {
First *int
Last *int
}

type admin interface {
IdResolver() string
NameResolver() string
RoleResolver() string
}

type user struct {
Id string
Name string
Role string
Email string
Phone string
Address *[]string
Friends *[]*user
}

func (u user) IdResolver() string {
return u.Id
}

func (u user) NameResolver() string {
return u.Name
}

func (u user) RoleResolver() string {
return u.Role
}

func (u user) FriendsResolver(args struct{ Page *page }) (*[]*user, error) {

from := 0
numFriends := len(*u.Friends)
to := numFriends

if args.Page != nil {
if args.Page.First != nil {
from = *args.Page.First
}
if args.Page.Last != nil {
to = *args.Page.Last
if to > numFriends {
to = numFriends
}
}
}

friends := (*u.Friends)[from:to]

return &friends, nil
}

var users = []*user{
{
Id: "0x01",
Name: "Albus Dumbledore",
Role: "ADMIN",
Email: "[email protected]",
Phone: "000-000-0000",
Address: &[]string{"Office @ Hogwarts", "where Horcruxes are"},
},
{
Id: "0x02",
Name: "Harry Potter",
Role: "USER",
Email: "[email protected]",
Phone: "000-000-0001",
Address: &[]string{"123 dorm room @ Hogwarts", "456 random place"},
},
{
Id: "0x03",
Name: "Hermione Granger",
Role: "USER",
Email: "[email protected]",
Phone: "000-000-0011",
Address: &[]string{"233 dorm room @ Hogwarts", "786 @ random place"},
},
{
Id: "0x04",
Name: "Ronald Weasley",
Role: "USER",
Email: "[email protected]",
Phone: "000-000-0111",
Address: &[]string{"411 dorm room @ Hogwarts", "981 @ random place"},
},
}

var usersMap = make(map[string]*user)

func init() {
users[0].Friends = &[]*user{users[1]}
users[1].Friends = &[]*user{users[0], users[2], users[3]}
users[2].Friends = &[]*user{users[1], users[3]}
users[3].Friends = &[]*user{users[1], users[2]}
for _, usr := range users {
usersMap[usr.Id] = usr
}
}

type Resolver struct{}

func (r *Resolver) Admin(ctx context.Context, args struct {
Id string
Role string
}) (admin, error) {
if usr, ok := usersMap[args.Id]; ok {
if usr.Role == args.Role {
return *usr, nil
}
}
err := fmt.Errorf("user with id=%s and role=%s does not exist", args.Id, args.Role)
return user{}, err
}

func (r *Resolver) User(ctx context.Context, args struct{ Id string }) (user, error) {
if usr, ok := usersMap[args.Id]; ok {
return *usr, nil
}
err := fmt.Errorf("user with id=%s does not exist", args.Id)
return user{}, err
}
10 changes: 8 additions & 2 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package graphql

import (
"context"
"fmt"

"encoding/json"
"fmt"

"github.com/graph-gophers/graphql-go/errors"
"github.com/graph-gophers/graphql-go/internal/common"
Expand Down Expand Up @@ -73,6 +72,13 @@ type Schema struct {
// SchemaOpt is an option to pass to ParseSchema or MustParseSchema.
type SchemaOpt func(*Schema)

// Specifies whether to use struct field resolvers
func UseFieldResolvers() SchemaOpt {
return func(s *Schema) {
s.schema.UseFieldResolvers = true
}
}

// MaxDepth specifies the maximum field nesting depth in a query. The default is 0 which disables max depth checking.
func MaxDepth(n int) SchemaOpt {
return func(s *Schema) {
Expand Down
42 changes: 26 additions & 16 deletions internal/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,22 +173,33 @@ func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *p
return errors.Errorf("%s", err) // don't execute any more resolvers if context got cancelled
}

var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(traceCtx))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}
callOut := f.resolver.Method(f.field.MethodIndex).Call(in)
result = callOut[0]
if f.field.HasError && !callOut[1].IsNil() {
resolverErr := callOut[1].Interface().(error)
err := errors.Errorf("%s", resolverErr)
err.Path = path.toSlice()
err.ResolverError = resolverErr
return err
if f.field.MethodIndex != -1 {
var in []reflect.Value
if f.field.HasContext {
in = append(in, reflect.ValueOf(traceCtx))
}
if f.field.ArgsPacker != nil {
in = append(in, f.field.PackedArgs)
}

callOut := f.resolver.Method(f.field.MethodIndex).Call(in)
result = callOut[0]
if f.field.HasError && !callOut[1].IsNil() {
resolverErr := callOut[1].Interface().(error)
err := errors.Errorf("%s", resolverErr)
err.Path = path.toSlice()
err.ResolverError = resolverErr
return err
}
} else {
// TODO extract out unwrapping ptr logic to a common place
res := f.resolver
if res.Kind() == reflect.Ptr {
res = res.Elem()
}
result = res.Field(f.field.FieldIndex)
}

return nil
}()

Expand All @@ -201,7 +212,6 @@ func execFieldSelection(ctx context.Context, r *Request, f *fieldToExec, path *p
f.out.WriteString("null") // TODO handle non-nil
return
}

r.execSelectionSet(traceCtx, f.sels, f.field.Type, path, result, f.out)
}

Expand Down
Loading