Skip to content

Commit

Permalink
Hand written codegen example
Browse files Browse the repository at this point in the history
  • Loading branch information
vektah committed Feb 3, 2018
1 parent 5a756bd commit 01896b3
Show file tree
Hide file tree
Showing 6 changed files with 582 additions and 0 deletions.
22 changes: 22 additions & 0 deletions example/todo/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

schema {
query: Query
mutation: Mutation
}

type Query {
todo(id: Integer!): Todo
lastTodo: Todo
todos: [Todos!]!
}

type Mutation {
createTodo(text: String!): Todo!
updateTodo(id: Integer!, text: String!): Todo!
}

type Todo @go(type:"github.com/99designs/graphql-go/example/todo.Todo") {
id: ID!
text: String!
done: Boolean!
}
83 changes: 83 additions & 0 deletions example/todo/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"encoding/json"
"log"
"net/http"

"fmt"

"github.com/vektah/graphql-go/example/todo"
"github.com/vektah/graphql-go/example/todo/todoresolver"
)

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

resolver := todo.NewResolver()

http.HandleFunc("/query", func(w http.ResponseWriter, r *http.Request) {
var params struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]interface{} `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

response := todoresolver.ExecuteRequest(resolver, params.Query, params.OperationName, params.Variables)
fmt.Println(string(response.Data))
responseJSON, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.Write(responseJSON)
})

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

var page = []byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.1.0/fetch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.5.4/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.10.2/graphiql.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>
`)
82 changes: 82 additions & 0 deletions example/todo/todo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
////go:generate graphgen -schema ./schema.graphql
//
package todo

import (
"errors"
)

type Todo struct {
ID int
Text string
Done bool
UserID int
}

type TodoResolver struct {
todos []*Todo
lastID int
}

func NewResolver() *TodoResolver {
return &TodoResolver{
todos: []*Todo{
{ID: 1, Text: "A todo not to forget", Done: false, UserID: 1},
{ID: 2, Text: "This is the most important", Done: false, UserID: 1},
{ID: 3, Text: "Please do this or else", Done: false, UserID: 1},
},
lastID: 3,
}
}

func (r *TodoResolver) Query_todo(id int) (*Todo, error) {
for _, todo := range r.todos {
if todo.ID == id {
return todo, nil
}
}
return nil, errors.New("not found")
}

func (r *TodoResolver) Query_lastTodo() (*Todo, error) {
if len(r.todos) == 0 {
return nil, errors.New("not found")
}
return r.todos[len(r.todos)-1], nil
}

func (r *TodoResolver) Query_todos() ([]*Todo, error) {
return r.todos, nil
}

func (r *TodoResolver) Mutation_createTodo(text string) (Todo, error) {
newID := r.id()

newTodo := Todo{
ID: newID,
Text: text,
Done: false,
}

r.todos = append(r.todos, &newTodo)

return newTodo, nil
}

func (r *TodoResolver) Mutation_updateTodo(id int, done bool) (*Todo, error) {
var affectedTodo *Todo

for i := 0; i < len(r.todos); i++ {
if r.todos[i].ID == id {
r.todos[i].Done = done
affectedTodo = r.todos[i]
break
}
}
return affectedTodo, errors.New("not found")
}

func (r *TodoResolver) id() int {
r.lastID++
return r.lastID
}
142 changes: 142 additions & 0 deletions example/todo/todoresolver/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package todoresolver

import (
"bytes"
"fmt"

"github.com/vektah/graphql-go/errors"
"github.com/vektah/graphql-go/internal/query"
"github.com/vektah/graphql-go/internal/validation"
"github.com/vektah/graphql-go/jsonw"
)

type ExecutionContext struct {
variables map[string]interface{}
errors []*errors.QueryError
resolvers Resolvers
}

type Type interface {
GetField(field string) Type
Execute(ec *ExecutionContext, object interface{}, field string, arguments map[string]interface{}, sels []query.Selection) jsonw.Encodable
}

func (c *ExecutionContext) errorf(format string, args ...interface{}) {
c.errors = append(c.errors, errors.Errorf(format, args...))
}

func (c *ExecutionContext) error(err error) {
c.errors = append(c.errors, errors.Errorf("%s", err.Error()))
}

func getOperation(document *query.Document, operationName string) (*query.Operation, error) {
if len(document.Operations) == 0 {
return nil, fmt.Errorf("no operations in query document")
}

if operationName == "" {
if len(document.Operations) > 1 {
return nil, fmt.Errorf("more than one operation in query document and no operation name given")
}
for _, op := range document.Operations {
return op, nil // return the one and only operation
}
}

op := document.Operations.Get(operationName)
if op == nil {
return nil, fmt.Errorf("no operation with name %q", operationName)
}
return op, nil
}

func ExecuteRequest(resolvers Resolvers, document string, operationName string, variables map[string]interface{}) *jsonw.Response {
doc, qErr := query.Parse(document)
if qErr != nil {
return &jsonw.Response{Errors: []*errors.QueryError{qErr}}
}

errs := validation.Validate(parsedSchema, doc)
if len(errs) != 0 {
return &jsonw.Response{Errors: errs}
}

op, err := getOperation(doc, operationName)
if err != nil {
return &jsonw.Response{Errors: []*errors.QueryError{errors.Errorf("%s", err)}}
}

// TODO: variable coercion?

c := ExecutionContext{
variables: variables,
resolvers: resolvers,
}

var rootType Type = queryType{}

if op.Type == query.Mutation {
rootType = mutationType{}
}

// TODO: parallelize if query.
data := c.executeSelectionSet(op.Selections, rootType, nil)
b := &bytes.Buffer{}
data.JSON(b)
return &jsonw.Response{
Data: b.Bytes(),
Errors: c.errors,
}
}

func (c *ExecutionContext) executeSelectionSet(sel []query.Selection, objectType Type, objectValue interface{}) jsonw.Encodable {
groupedFieldSet := c.collectFields(objectType, sel, map[string]interface{}{})
fmt.Println("ESS grouped selections")
for _, s := range groupedFieldSet {
fmt.Println(s.Alias)
}
resultMap := jsonw.Map{}

for _, collectedField := range groupedFieldSet {
//fieldType := objectType.GetField(collectedField.Name)
//if fieldType == nil {
// continue
//}
resultMap.Set(collectedField.Alias, objectType.Execute(c, objectValue, collectedField.Name, map[string]interface{}{}, collectedField.Selections))
}
return resultMap
}

type CollectedField struct {
Alias string
Name string
Selections []query.Selection
}

func findField(c *[]CollectedField, alias string, name string) *CollectedField {
for i, cf := range *c {
if cf.Alias == alias {
return &(*c)[i]
}
}

*c = append(*c, CollectedField{Alias: alias, Name: name})
return &(*c)[len(*c)-1]
}

func (c *ExecutionContext) collectFields(objectType Type, selSet []query.Selection, visited map[string]interface{}) []CollectedField {
var groupedFields []CollectedField

// TODO: Basically everything.
for _, sel := range selSet {
switch sel := sel.(type) {
case *query.Field:
f := findField(&groupedFields, sel.Alias.Name, sel.Name.Name)
f.Selections = append(f.Selections, sel.Selections...)
default:
panic("Unsupported!")
}
}

return groupedFields
}
Loading

0 comments on commit 01896b3

Please sign in to comment.