From 01896b3bbd66b3e6131ab1242c34cc5b1761d78a Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Sat, 3 Feb 2018 19:29:36 +1100 Subject: [PATCH] Hand written codegen example --- example/todo/schema.graphql | 22 ++++ example/todo/server/server.go | 83 +++++++++++++ example/todo/todo.go | 82 +++++++++++++ example/todo/todoresolver/exec.go | 142 ++++++++++++++++++++++ example/todo/todoresolver/generated.go | 156 +++++++++++++++++++++++++ jsonw/output.go | 97 +++++++++++++++ 6 files changed, 582 insertions(+) create mode 100644 example/todo/schema.graphql create mode 100644 example/todo/server/server.go create mode 100644 example/todo/todo.go create mode 100644 example/todo/todoresolver/exec.go create mode 100644 example/todo/todoresolver/generated.go create mode 100644 jsonw/output.go diff --git a/example/todo/schema.graphql b/example/todo/schema.graphql new file mode 100644 index 00000000000..f1e4db1c1b7 --- /dev/null +++ b/example/todo/schema.graphql @@ -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! +} diff --git a/example/todo/server/server.go b/example/todo/server/server.go new file mode 100644 index 00000000000..268c6914639 --- /dev/null +++ b/example/todo/server/server.go @@ -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(¶ms); 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(` + + + + + + + + + + +
Loading...
+ + + +`) diff --git a/example/todo/todo.go b/example/todo/todo.go new file mode 100644 index 00000000000..f8b2c57a6d4 --- /dev/null +++ b/example/todo/todo.go @@ -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 +} diff --git a/example/todo/todoresolver/exec.go b/example/todo/todoresolver/exec.go new file mode 100644 index 00000000000..18d69f9a6d7 --- /dev/null +++ b/example/todo/todoresolver/exec.go @@ -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 +} diff --git a/example/todo/todoresolver/generated.go b/example/todo/todoresolver/generated.go new file mode 100644 index 00000000000..7e47302e000 --- /dev/null +++ b/example/todo/todoresolver/generated.go @@ -0,0 +1,156 @@ +package todoresolver + +import ( + "fmt" + + "github.com/vektah/graphql-go/example/todo" + "github.com/vektah/graphql-go/internal/query" + "github.com/vektah/graphql-go/internal/schema" + "github.com/vektah/graphql-go/jsonw" +) + +type Resolvers interface { + Query_todo(id int) (*todo.Todo, error) + Query_lastTodo() (*todo.Todo, error) + Query_todos() ([]*todo.Todo, error) + + Mutation_createTodo(text string) (todo.Todo, error) + Mutation_updateTodo(id int, done bool) (*todo.Todo, error) +} + +type ( + queryType struct{} + mutationType struct{} + todoType struct{} +) + +func (q queryType) GetField(field string) Type { + switch field { + case "todo": + return todoType{} + case "lastTodo": + return todoType{} + case "todos": + return todoType{} + } + return nil +} + +func (q queryType) Execute(ec *ExecutionContext, object interface{}, field string, arguments map[string]interface{}, sels []query.Selection) jsonw.Encodable { + fmt.Println("query::exec") + switch field { + case "todo": + result, err := ec.resolvers.Query_todo(arguments["id"].(int)) + if err != nil { + ec.error(err) + return jsonw.Null + } + return ec.executeSelectionSet(sels, todoType{}, result) + + case "lastTodo": + result, err := ec.resolvers.Query_lastTodo() + if err != nil { + ec.error(err) + return jsonw.Null + } + return ec.executeSelectionSet(sels, todoType{}, result) + + case "todos": + result, err := ec.resolvers.Query_todos() + if err != nil { + ec.error(err) + return jsonw.Null + } + + var enc jsonw.Array + for _, val := range result { + enc = append(enc, ec.executeSelectionSet(sels, todoType{}, val)) + } + + return enc + } + + panic("unknown field " + field) +} + +func (q mutationType) GetField(field string) Type { + switch field { + case "createTodo": + return todoType{} + case "updateTodo": + return todoType{} + } + return nil +} + +func (q mutationType) Execute(ec *ExecutionContext, object interface{}, field string, arguments map[string]interface{}, sels []query.Selection) jsonw.Encodable { + switch field { + case "createTodo": + result, err := ec.resolvers.Mutation_createTodo(arguments["text"].(string)) + if err != nil { + ec.error(err) + return jsonw.Null + } + return ec.executeSelectionSet(sels, todoType{}, result) + + case "updateTodo": + result, err := ec.resolvers.Mutation_updateTodo(arguments["id"].(int), arguments["done"].(bool)) + if err != nil { + ec.error(err) + return jsonw.Null + } + return ec.executeSelectionSet(sels, todoType{}, result) + } + + panic("unknown field " + field) +} + +func (q todoType) GetField(field string) Type { + return nil +} + +func (q todoType) Execute(ec *ExecutionContext, object interface{}, field string, arguments map[string]interface{}, sels []query.Selection) jsonw.Encodable { + fmt.Print("todoExec", object) + switch field { + case "id": + return jsonw.Int(object.(*todo.Todo).ID) + case "text": + return jsonw.String(object.(*todo.Todo).Text) + case "done": + return jsonw.Bool(object.(*todo.Todo).Done) + } + return jsonw.Null +} + +var parsedSchema *schema.Schema + +const schemaStr = `schema { + query: Query + mutation: Mutation +} +type Query { + todo(id: Integer!): Todo + lastTodo: Todo + todos: [Todos!]! + user(id: Integer!): User +} +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! + user: User! +} +type User @go(type:"github.com/99designs/graphql-go/example/todo.User"){ + id: ID! + name: String! +} +` + +func init() { + parsedSchema = schema.New() + parsedSchema.Resolve(schemaStr) +} diff --git a/jsonw/output.go b/jsonw/output.go new file mode 100644 index 00000000000..d27a3b2c45b --- /dev/null +++ b/jsonw/output.go @@ -0,0 +1,97 @@ +package jsonw + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + + "github.com/vektah/graphql-go/errors" +) + +var Null = literal{[]byte(`null`)} +var True = literal{[]byte(`true`)} +var False = literal{[]byte(`false`)} + +var openParen = []byte(`{`) +var closeParen = []byte(`}`) +var openBracket = []byte(`[`) +var closeBracket = []byte(`]`) +var colon = []byte(`:`) +var comma = []byte(`,`) + +type Encodable interface { + JSON(w io.Writer) +} + +type Map []struct { + Name string + Value Encodable +} + +func (r *Map) Set(name string, value Encodable) { + *r = append(*r, struct { + Name string + Value Encodable + }{name, value}) +} + +func (r Map) JSON(w io.Writer) { + w.Write(openParen) + + for i, f := range r { + if i > 0 { + w.Write(comma) + } + io.WriteString(w, strconv.Quote(f.Name)) + + w.Write(colon) + f.Value.JSON(w) + } + w.Write(closeParen) +} + +type Response struct { + Data json.RawMessage `json:"data,omitempty"` + Errors []*errors.QueryError `json:"errors,omitempty"` + Extensions map[string]interface{} `json:"extensions,omitempty"` +} + +type literal struct { + value []byte +} + +func (r literal) JSON(w io.Writer) { + w.Write(r.value) +} + +func Int(v int) Encodable { + return literal{[]byte(fmt.Sprintf("%d", v))} +} + +func String(v string) Encodable { + return literal{[]byte(strconv.Quote(v))} +} + +func Bool(v bool) Encodable { + if v { + return True + } else { + return False + } +} + +type Array []Encodable + +func (r Array) JSON(w io.Writer) { + w.Write(openBracket) + + for i, f := range r { + if i > 0 { + w.Write(comma) + } + + f.JSON(w) + } + w.Write(closeBracket) +}