From fbd6f2ede7c8a3107e63cb633d09c86c99940ce2 Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Thu, 22 Feb 2018 19:15:35 +1100 Subject: [PATCH] Generate input models too --- README.md | 11 ++++++- codegen/build.go | 6 ++-- codegen/object_build.go | 3 +- codegen/templates/args.gotpl | 29 ++++++------------ codegen/templates/data.go | 2 +- example/todo/generated.go | 50 +++++++++++++++++++++++++++---- example/todo/schema.graphql | 6 ++-- example/todo/todo.go | 11 ++++--- example/todo/todo_test.go | 2 +- example/todo/types.json | 3 -- graphql/map.go | 24 +++++++++++++++ main.go | 1 + neelance/schema/meta.go | 3 ++ neelance/validation/validation.go | 3 ++ 14 files changed, 112 insertions(+), 42 deletions(-) delete mode 100644 example/todo/types.json create mode 100644 graphql/map.go diff --git a/README.md b/README.md index 0e02d724bff..d2cf50ccade 100644 --- a/README.md +++ b/README.md @@ -237,10 +237,19 @@ This magic comment tells `go generate` what command to run when we want to regen go generate ./.. ``` -*gorunpkg* will build and run the version of gqlgen we just installed into vendor with dep. This makes sure +*gorunpkg* will build and run the version of gqlgen we just installed into vendor with dep. This makes sure that everyone working on your project generates code the same way regardless which binaries are installed in their gopath. +### Included custom scalar types + +Included in gqlgen there are some custom scalar types that will just work out of the box. + +- Time: An RFC3339 date as a quoted string +- Map: a json object represented as a map[string]interface{}. Useful change sets. + +You are free to redefine these any way you want in types.json, see the [custom scalar example](./example/scalars). + ### Prior art #### neelance/graphql-go diff --git a/codegen/build.go b/codegen/build.go index 751170cb65b..15f0282715f 100644 --- a/codegen/build.go +++ b/codegen/build.go @@ -36,13 +36,15 @@ func Bind(schema *schema.Schema, userTypes map[string]string, destDir string) (* bindTypes(imports, namedTypes, prog) objects := buildObjects(namedTypes, schema, prog, imports) + inputs := buildInputs(namedTypes, schema, prog, imports) + models := append(findMissing(objects), findMissing(inputs)...) b := &Build{ PackageName: filepath.Base(destDir), Objects: objects, - Models: findMissingObjects(objects, schema), + Models: models, Interfaces: buildInterfaces(namedTypes, schema), - Inputs: buildInputs(namedTypes, schema, prog, imports), + Inputs: inputs, Imports: imports, } diff --git a/codegen/object_build.go b/codegen/object_build.go index d3876c7e1f8..0aa170fee56 100644 --- a/codegen/object_build.go +++ b/codegen/object_build.go @@ -48,7 +48,7 @@ func buildObjects(types NamedTypes, s *schema.Schema, prog *loader.Program, impo return objects } -func findMissingObjects(objects Objects, s *schema.Schema) Objects { +func findMissing(objects Objects) Objects { var missingObjects Objects for _, object := range objects { @@ -56,6 +56,7 @@ func findMissingObjects(objects Objects, s *schema.Schema) Objects { continue } object.GoType = ucFirst(object.GQLType) + object.Marshaler = &Ref{GoType: object.GoType} for i := range object.Fields { field := &object.Fields[i] diff --git a/codegen/templates/args.gotpl b/codegen/templates/args.gotpl index ed08382898d..e2d253d0ba6 100644 --- a/codegen/templates/args.gotpl +++ b/codegen/templates/args.gotpl @@ -1,26 +1,15 @@ {{- range $i, $arg := . }} var arg{{$i}} {{$arg.Signature }} - {{- if eq $arg.GoType "map[string]interface{}" }} - if tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok { - {{- if $arg.Type.IsPtr }} - tmp2 := tmp.({{$arg.GoType}}) - arg{{$i}} = &tmp2 + if tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok { + var err error + {{$arg.Unmarshal (print "arg" $i) "tmp" }} + if err != nil { + ec.Error(err) + {{- if $arg.Object.Stream }} + return nil {{- else }} - arg{{$i}} = tmp.({{$arg.GoType}}) + return graphql.Null {{- end }} } - {{- else}} - if tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok { - var err error - {{$arg.Unmarshal (print "arg" $i) "tmp" }} - if err != nil { - ec.Error(err) - {{- if $arg.Object.Stream }} - return nil - {{- else }} - return graphql.Null - {{- end }} - } - } - {{- end}} + } {{- end -}} diff --git a/codegen/templates/data.go b/codegen/templates/data.go index 3ac96bb8803..2702e85a6a4 100644 --- a/codegen/templates/data.go +++ b/codegen/templates/data.go @@ -1,7 +1,7 @@ package templates var data = map[string]string{ - "args.gotpl": "\t{{- range $i, $arg := . }}\n\t\tvar arg{{$i}} {{$arg.Signature }}\n\t\t{{- if eq $arg.GoType \"map[string]interface{}\" }}\n\t\t\tif tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok {\n\t\t\t\t{{- if $arg.Type.IsPtr }}\n\t\t\t\t\ttmp2 := tmp.({{$arg.GoType}})\n\t\t\t\t\targ{{$i}} = &tmp2\n\t\t\t\t{{- else }}\n\t\t\t\t\targ{{$i}} = tmp.({{$arg.GoType}})\n\t\t\t\t{{- end }}\n\t\t\t}\n\t\t{{- else}}\n\t\t\tif tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok {\n\t\t\t\tvar err error\n\t\t\t\t{{$arg.Unmarshal (print \"arg\" $i) \"tmp\" }}\n\t\t\t\tif err != nil {\n\t\t\t\t\tec.Error(err)\n\t\t\t\t\t{{- if $arg.Object.Stream }}\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t{{- else }}\n\t\t\t\t\t\treturn graphql.Null\n\t\t\t\t\t{{- end }}\n\t\t\t\t}\n\t\t\t}\n\t\t{{- end}}\n\t{{- end -}}\n", + "args.gotpl": "\t{{- range $i, $arg := . }}\n\t\tvar arg{{$i}} {{$arg.Signature }}\n\t\tif tmp, ok := field.Args[{{$arg.GQLName|quote}}]; ok {\n\t\t\tvar err error\n\t\t\t{{$arg.Unmarshal (print \"arg\" $i) \"tmp\" }}\n\t\t\tif err != nil {\n\t\t\t\tec.Error(err)\n\t\t\t\t{{- if $arg.Object.Stream }}\n\t\t\t\t\treturn nil\n\t\t\t\t{{- else }}\n\t\t\t\t\treturn graphql.Null\n\t\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\t{{- end -}}\n", "file.gotpl": "// This file was generated by github.com/vektah/gqlgen, DO NOT EDIT\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\nfunc MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema {\n\treturn &executableSchema{resolvers}\n}\n\ntype Resolvers interface {\n{{- range $object := .Objects -}}\n\t{{ range $field := $object.Fields -}}\n\t\t{{ $field.ResolverDeclaration }}\n\t{{ end }}\n{{- end }}\n}\n\n{{ range $model := .Models }}\n\t{{ template \"model.gotpl\" $model }}\n{{- end}}\n\ntype executableSchema struct {\n\tresolvers Resolvers\n}\n\nfunc (e *executableSchema) Schema() *schema.Schema {\n\treturn parsedSchema\n}\n\nfunc (e *executableSchema) Query(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response {\n\t{{- if .QueryRoot }}\n\t\tec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx}\n\n\t\tdata := ec._{{.QueryRoot.GQLType|lcFirst}}(op.Selections)\n\t\tec.wg.Wait()\n\n\t\treturn &graphql.Response{\n\t\t\tData: data,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn &graphql.Response{Errors: []*errors.QueryError{ {Message: \"queries are not supported\"} }}\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Mutation(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) *graphql.Response {\n\t{{- if .MutationRoot }}\n\t\tec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx}\n\n\t\tdata := ec._{{.MutationRoot.GQLType|lcFirst}}(op.Selections)\n\t\tec.wg.Wait()\n\n\t\treturn &graphql.Response{\n\t\t\tData: data,\n\t\t\tErrors: ec.Errors,\n\t\t}\n\t{{- else }}\n\t\treturn &graphql.Response{Errors: []*errors.QueryError{ {Message: \"mutations are not supported\"} }}\n\t{{- end }}\n}\n\nfunc (e *executableSchema) Subscription(ctx context.Context, doc *query.Document, variables map[string]interface{}, op *query.Operation) <-chan *graphql.Response {\n\t{{- if .SubscriptionRoot }}\n\t\tevents := make(chan *graphql.Response, 10)\n\n\t\tec := executionContext{resolvers: e.resolvers, variables: variables, doc: doc, ctx: ctx}\n\n\t\teventData := ec._{{.SubscriptionRoot.GQLType|lcFirst}}(op.Selections)\n\t\tif ec.Errors != nil {\n\t\t\tevents<-&graphql.Response{\n\t\t\t\tData: graphql.Null,\n\t\t\t\tErrors: ec.Errors,\n\t\t\t}\n\t\t\tclose(events)\n\t\t} else {\n\t\t\tgo func() {\n\t\t\t\tfor data := range eventData {\n\t\t\t\t\tec.wg.Wait()\n\t\t\t\t\tevents <- &graphql.Response{\n\t\t\t\t\t\tData: data,\n\t\t\t\t\t\tErrors: ec.Errors,\n\t\t\t\t\t}\n\t\t\t\t\ttime.Sleep(20 * time.Millisecond)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t\treturn events\n\t{{- else }}\n\t\tevents := make(chan *graphql.Response, 1)\n\t\tevents<-&graphql.Response{Errors: []*errors.QueryError{ {Message: \"subscriptions are not supported\"} }}\n\t\treturn events\n\t{{- end }}\n}\n\ntype executionContext struct {\n\terrors.Builder\n\tresolvers Resolvers\n\tvariables map[string]interface{}\n\tdoc *query.Document\n\tctx context.Context\n\twg sync.WaitGroup\n}\n\n{{- range $object := .Objects }}\n\t{{ template \"object.gotpl\" $object }}\n{{- end}}\n\n{{- range $interface := .Interfaces }}\n\t{{ template \"interface.gotpl\" $interface }}\n{{- end }}\n\n{{- range $input := .Inputs }}\n\t{{ template \"input.gotpl\" $input }}\n{{- end }}\n\nvar parsedSchema = schema.MustParse({{.SchemaRaw|quote}})\n\nfunc (ec *executionContext) introspectSchema() *introspection.Schema {\n\treturn introspection.WrapSchema(parsedSchema)\n}\n\nfunc (ec *executionContext) introspectType(name string) *introspection.Type {\n\tt := parsedSchema.Resolve(name)\n\tif t == nil {\n\t\treturn nil\n\t}\n\treturn introspection.WrapType(t)\n}\n", "input.gotpl": "\t{{- if .IsMarshaled }}\n\tfunc Unmarshal{{ .GQLType }}(v interface{}) ({{.FullName}}, error) {\n\t\tvar it {{.FullName}}\n\n\t\tfor k, v := range v.(map[string]interface{}) {\n\t\t\tswitch k {\n\t\t\t{{- range $field := .Fields }}\n\t\t\tcase {{$field.GQLName|quote}}:\n\t\t\t\tvar err error\n\t\t\t\t{{ $field.Unmarshal (print \"it.\" $field.GoVarName) \"v\" }}\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn it, err\n\t\t\t\t}\n\t\t\t{{- end }}\n\t\t\t}\n\t\t}\n\n\t\treturn it, nil\n\t}\n\t{{- end }}\n", "interface.gotpl": "{{- $interface := . }}\n\nfunc (ec *executionContext) _{{$interface.GQLType|lcFirst}}(sel []query.Selection, it *{{$interface.FullName}}) graphql.Marshaler {\n\tswitch it := (*it).(type) {\n\tcase nil:\n\t\treturn graphql.Null\n\t{{- range $implementor := $interface.Implementors }}\n\tcase {{$implementor.FullName}}:\n\t\treturn ec._{{$implementor.GQLType|lcFirst}}(sel, &it)\n\n\tcase *{{$implementor.FullName}}:\n\t\treturn ec._{{$implementor.GQLType|lcFirst}}(sel, it)\n\n\t{{- end }}\n\tdefault:\n\t\tpanic(fmt.Errorf(\"unexpected type %T\", it))\n\t}\n}\n", diff --git a/example/todo/generated.go b/example/todo/generated.go index ec59b3f34cf..abfa348a650 100644 --- a/example/todo/generated.go +++ b/example/todo/generated.go @@ -19,7 +19,7 @@ func MakeExecutableSchema(resolvers Resolvers) graphql.ExecutableSchema { } type Resolvers interface { - MyMutation_createTodo(ctx context.Context, text string) (Todo, error) + MyMutation_createTodo(ctx context.Context, todo TodoInput) (Todo, error) MyMutation_updateTodo(ctx context.Context, id int, changes map[string]interface{}) (*Todo, error) MyQuery_todo(ctx context.Context, id int) (*Todo, error) MyQuery_lastTodo(ctx context.Context) (*Todo, error) @@ -32,6 +32,11 @@ type Todo struct { Done bool } +type TodoInput struct { + Text string + Done *bool +} + type executableSchema struct { resolvers Resolvers } @@ -93,11 +98,11 @@ func (ec *executionContext) _myMutation(sel []query.Selection) graphql.Marshaler case "__typename": out.Values[i] = graphql.MarshalString("MyMutation") case "createTodo": - var arg0 string - if tmp, ok := field.Args["text"]; ok { + var arg0 TodoInput + if tmp, ok := field.Args["todo"]; ok { var err error - arg0, err = graphql.UnmarshalString(tmp) + arg0, err = UnmarshalTodoInput(tmp) if err != nil { ec.Error(err) return graphql.Null @@ -123,7 +128,13 @@ func (ec *executionContext) _myMutation(sel []query.Selection) graphql.Marshaler } var arg1 map[string]interface{} if tmp, ok := field.Args["changes"]; ok { - arg1 = tmp.(map[string]interface{}) + var err error + + arg1, err = graphql.UnmarshalMap(tmp) + if err != nil { + ec.Error(err) + return graphql.Null + } } res, err := ec.resolvers.MyMutation_updateTodo(ec.ctx, arg0, arg1) if err != nil { @@ -723,7 +734,34 @@ func (ec *executionContext) ___Type(sel []query.Selection, it *introspection.Typ return out } -var parsedSchema = schema.MustParse("schema {\n\tquery: MyQuery\n\tmutation: MyMutation\n}\n\ntype MyQuery {\n\ttodo(id: Int!): Todo\n\tlastTodo: Todo\n\ttodos: [Todo!]!\n}\n\ntype MyMutation {\n\tcreateTodo(text: String!): Todo!\n\tupdateTodo(id: Int!, changes: TodoInput!): Todo\n}\n\ntype Todo {\n\tid: Int!\n\ttext: String!\n\tdone: Boolean!\n}\n\ninput TodoInput {\n\ttext: String\n\tdone: Boolean\n}\n") +func UnmarshalTodoInput(v interface{}) (TodoInput, error) { + var it TodoInput + + for k, v := range v.(map[string]interface{}) { + switch k { + case "text": + var err error + + it.Text, err = graphql.UnmarshalString(v) + if err != nil { + return it, err + } + case "done": + var err error + var ptr1 bool + + ptr1, err = graphql.UnmarshalBoolean(v) + it.Done = &ptr1 + if err != nil { + return it, err + } + } + } + + return it, nil +} + +var parsedSchema = schema.MustParse("schema {\n\tquery: MyQuery\n\tmutation: MyMutation\n}\n\ntype MyQuery {\n\ttodo(id: Int!): Todo\n\tlastTodo: Todo\n\ttodos: [Todo!]!\n}\n\ntype MyMutation {\n\tcreateTodo(todo: TodoInput!): Todo!\n\tupdateTodo(id: Int!, changes: Map!): Todo\n}\n\ntype Todo {\n\tid: Int!\n\ttext: String!\n\tdone: Boolean!\n}\n\ninput TodoInput {\n\ttext: String!\n\tdone: Boolean\n}\n") func (ec *executionContext) introspectSchema() *introspection.Schema { return introspection.WrapSchema(parsedSchema) diff --git a/example/todo/schema.graphql b/example/todo/schema.graphql index 8f71e76e9d3..2977e89cddd 100644 --- a/example/todo/schema.graphql +++ b/example/todo/schema.graphql @@ -10,8 +10,8 @@ type MyQuery { } type MyMutation { - createTodo(text: String!): Todo! - updateTodo(id: Int!, changes: TodoInput!): Todo + createTodo(todo: TodoInput!): Todo! + updateTodo(id: Int!, changes: Map!): Todo } type Todo { @@ -21,6 +21,6 @@ type Todo { } input TodoInput { - text: String + text: String! done: Boolean } diff --git a/example/todo/todo.go b/example/todo/todo.go index bb343f6edea..5ae218b917e 100644 --- a/example/todo/todo.go +++ b/example/todo/todo.go @@ -1,4 +1,4 @@ -//go:generate gorunpkg github.com/vektah/gqlgen -typemap types.json -out generated.go +//go:generate gorunpkg github.com/vektah/gqlgen -out generated.go package todo @@ -47,13 +47,16 @@ func (r *todoResolver) MyQuery_todos(ctx context.Context) ([]Todo, error) { return r.todos, nil } -func (r *todoResolver) MyMutation_createTodo(ctx context.Context, text string) (Todo, error) { +func (r *todoResolver) MyMutation_createTodo(ctx context.Context, todo TodoInput) (Todo, error) { newID := r.id() newTodo := Todo{ ID: newID, - Text: text, - Done: false, + Text: todo.Text, + } + + if todo.Done != nil { + newTodo.Done = *todo.Done } r.todos = append(r.todos, newTodo) diff --git a/example/todo/todo_test.go b/example/todo/todo_test.go index 897e3d3e49c..ad6c27452c4 100644 --- a/example/todo/todo_test.go +++ b/example/todo/todo_test.go @@ -19,7 +19,7 @@ func TestTodo(t *testing.T) { var resp struct { CreateTodo struct{ ID int } } - c.MustPost(`mutation { createTodo(text:"Fery important") { id } }`, &resp) + c.MustPost(`mutation { createTodo(todo:{text:"Fery important"}) { id } }`, &resp) require.Equal(t, 4, resp.CreateTodo.ID) }) diff --git a/example/todo/types.json b/example/todo/types.json deleted file mode 100644 index d9e2d575e39..00000000000 --- a/example/todo/types.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "TodoInput": "map[string]interface{}" -} diff --git a/graphql/map.go b/graphql/map.go new file mode 100644 index 00000000000..1e91d1d98c1 --- /dev/null +++ b/graphql/map.go @@ -0,0 +1,24 @@ +package graphql + +import ( + "encoding/json" + "fmt" + "io" +) + +func MarshalMap(val map[string]interface{}) Marshaler { + return WriterFunc(func(w io.Writer) { + err := json.NewEncoder(w).Encode(val) + if err != nil { + panic(err) + } + }) +} + +func UnmarshalMap(v interface{}) (map[string]interface{}, error) { + if m, ok := v.(map[string]interface{}); ok { + return m, nil + } + + return nil, fmt.Errorf("%T is not a map", v) +} diff --git a/main.go b/main.go index 792458faf35..6d51892da0b 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,7 @@ func loadTypeMap() map[string]string { "Boolean": "github.com/vektah/gqlgen/graphql.Boolean", "ID": "github.com/vektah/gqlgen/graphql.ID", "Time": "github.com/vektah/gqlgen/graphql.Time", + "Map": "github.com/vektah/gqlgen/graphql.Map", } if *typemap != "" { b, err := ioutil.ReadFile(*typemap) diff --git a/neelance/schema/meta.go b/neelance/schema/meta.go index b48bf7acf28..efdcaa2c493 100644 --- a/neelance/schema/meta.go +++ b/neelance/schema/meta.go @@ -26,6 +26,9 @@ var metaSrc = ` # The ` + "`" + `ID` + "`" + ` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as ` + "`" + `"4"` + "`" + `) or integer (such as ` + "`" + `4` + "`" + `) input value will be accepted as an ID. scalar ID + # The ` + "`" + `Map` + "`" + ` scalar type is a simple json object + scalar Map + # Directs the executor to include this field or fragment only when the ` + "`" + `if` + "`" + ` argument is true. directive @include( # Included when true. diff --git a/neelance/validation/validation.go b/neelance/validation/validation.go index be94131acc5..28124310a00 100644 --- a/neelance/validation/validation.go +++ b/neelance/validation/validation.go @@ -672,6 +672,9 @@ func validateValueType(c *opContext, v common.Literal, t common.Type) (bool, str if validateBasicLit(lit, t) { return true, "" } + } else { + // custom complex scalars will be validated when unmarshaling + return true, "" } case *common.List: