Skip to content

Commit

Permalink
Merge pull request #1 from DealTap/impl-field-resolver
Browse files Browse the repository at this point in the history
Use Struct Field Resolver instead of Method
  • Loading branch information
0xSalman authored Apr 11, 2018
2 parents 00d3035 + de4e1e1 commit 2fcff20
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 75 deletions.
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

0 comments on commit 2fcff20

Please sign in to comment.