diff --git a/cmd/init.go b/cmd/init.go index fabe6a18d8d..66a6c073889 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -63,11 +63,11 @@ var initCmd = &cobra.Command{ initSchema() config := initConfig() - generateGraph(config) + GenerateGraphServer(config) }, } -func generateGraph(config *codegen.Config) { +func GenerateGraphServer(config *codegen.Config) { schemaRaw, err := ioutil.ReadFile(config.SchemaFilename) if err != nil { fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error()) @@ -80,10 +80,21 @@ func generateGraph(config *codegen.Config) { os.Exit(1) } + if serverFilename == "" { + serverFilename = "server/server.go" + } + if err := codegen.Generate(*config); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } + + if err := codegen.GenerateServer(*config, serverFilename); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + fmt.Fprintln(os.Stdout, `Exec "go run ./server/server.go" to start GraphQL server`) } func initConfig() *codegen.Config { diff --git a/cmd/root.go b/cmd/root.go index b9022d12275..ddd02f18b40 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,9 +18,11 @@ var schemaFilename string var typemap string var packageName string var modelPackageName string +var serverFilename string func init() { rootCmd.PersistentFlags().StringVarP(&configFilename, "config", "c", "", "the file to configuration to") + rootCmd.PersistentFlags().StringVarP(&serverFilename, "server", "s", "", "the file to write server to") rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show logs") rootCmd.PersistentFlags().StringVar(&output, "out", "", "the file to write to") diff --git a/codegen/build.go b/codegen/build.go index 12cab494255..a2f29d4ed48 100644 --- a/codegen/build.go +++ b/codegen/build.go @@ -39,6 +39,13 @@ type ResolverBuild struct { ResolverFound bool } +type ServerBuild struct { + PackageName string + Imports []*Import + ExecPackageName string + ResolverPackageName string +} + // Create a list of models that need to be generated func (cfg *Config) models() (*ModelBuild, error) { namedTypes := cfg.buildNamedTypes() @@ -99,6 +106,19 @@ func (cfg *Config) resolver() (*ResolverBuild, error) { }, nil } +func (cfg *Config) server(destDir string) *ServerBuild { + imports := buildImports(NamedTypes{}, destDir) + imports.add(cfg.Exec.ImportPath()) + imports.add(cfg.Resolver.ImportPath()) + + return &ServerBuild{ + PackageName: cfg.Resolver.Package, + Imports: imports.finalize(), + ExecPackageName: cfg.Exec.Package, + ResolverPackageName: cfg.Resolver.Package, + } +} + // bind a schema together with some code to generate a Build func (cfg *Config) bind() (*Build, error) { namedTypes := cfg.buildNamedTypes() diff --git a/codegen/codegen.go b/codegen/codegen.go index 6930b640070..89d6bfda694 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -66,6 +66,28 @@ func Generate(cfg Config) error { return nil } +func GenerateServer(cfg Config, filename string) error { + if err := cfg.Exec.normalize(); err != nil { + return errors.Wrap(err, "exec") + } + if err := cfg.Resolver.normalize(); err != nil { + return errors.Wrap(err, "resolver") + } + + serverFilename := abs(filename) + serverBuild := cfg.server(filepath.Dir(serverFilename)) + + if _, err := os.Stat(serverFilename); os.IsNotExist(errors.Cause(err)) { + err = templates.RenderToFile("server.gotpl", serverFilename, serverBuild) + if err != nil { + return errors.Wrap(err, "generate server failed") + } + } else { + log.Printf("Skipped server: %s already exists\n", serverFilename) + } + return nil +} + func generateResolver(cfg Config) error { resolverBuild, err := cfg.resolver() if err != nil { diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go new file mode 100644 index 00000000000..0c00888db95 --- /dev/null +++ b/codegen/codegen_test.go @@ -0,0 +1,43 @@ +package codegen + +import ( + "syscall" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/tools/go/loader" +) + +func TestGenerateServer(t *testing.T) { + name := "graphserver" + schema := ` + type Query { + user(): User + } + type User { + id: Int + } +` + serverFilename := "tests/gen/" + name + "/server/server.go" + cfg := Config{ + SchemaStr: schema, + Exec: PackageConfig{Filename: "tests/gen/" + name + "/exec.go"}, + Model: PackageConfig{Filename: "tests/gen/" + name + "/model.go"}, + Resolver: PackageConfig{Filename: "tests/gen/" + name + "/resolver.go", Type: "Resolver"}, + } + + _ = syscall.Unlink(cfg.Resolver.Filename) + _ = syscall.Unlink(serverFilename) + + err := Generate(cfg) + require.NoError(t, err) + + err = GenerateServer(cfg, serverFilename) + require.NoError(t, err) + + conf := loader.Config{} + conf.CreateFromFilenames("tests/gen/"+name, serverFilename) + + _, err = conf.Load() + require.NoError(t, err) +} diff --git a/codegen/templates/data.go b/codegen/templates/data.go index 6319d69c93a..6c229f8045f 100644 --- a/codegen/templates/data.go +++ b/codegen/templates/data.go @@ -9,4 +9,5 @@ var data = map[string]string{ "models.gotpl": "// Code 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\n{{ range $model := .Models }}\n\t{{- if .IsInterface }}\n\t\ttype {{.GoType}} interface {}\n\t{{- else }}\n\t\ttype {{.GoType}} struct {\n\t\t\t{{- range $field := .Fields }}\n\t\t\t\t{{- if $field.GoVarName }}\n\t\t\t\t\t{{ $field.GoVarName }} {{$field.Signature}} `json:\"{{$field.GQLName}}\"`\n\t\t\t\t{{- else }}\n\t\t\t\t\t{{ $field.GoFKName }} {{$field.GoFKType}}\n\t\t\t\t{{- end }}\n\t\t\t{{- end }}\n\t\t}\n\t{{- end }}\n{{- end}}\n\n{{ range $enum := .Enums }}\n\ttype {{.GoType}} string\n\tconst (\n\t{{ range $value := .Values -}}\n\t\t{{with .Description}} {{.|prefixLines \"// \"}} {{end}}\n\t\t{{$enum.GoType}}{{ .Name|toCamel }} {{$enum.GoType}} = {{.Name|quote}}\n\t{{- end }}\n\t)\n\n\tfunc (e {{.GoType}}) IsValid() bool {\n\t\tswitch e {\n\t\tcase {{ range $index, $element := .Values}}{{if $index}},{{end}}{{ $enum.GoType }}{{ $element.Name|toCamel }}{{end}}:\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t}\n\n\tfunc (e {{.GoType}}) String() string {\n\t\treturn string(e)\n\t}\n\n\tfunc (e *{{.GoType}}) UnmarshalGQL(v interface{}) error {\n\t\tstr, ok := v.(string)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"enums must be strings\")\n\t\t}\n\n\t\t*e = {{.GoType}}(str)\n\t\tif !e.IsValid() {\n\t\t\treturn fmt.Errorf(\"%s is not a valid {{.GQLType}}\", str)\n\t\t}\n\t\treturn nil\n\t}\n\n\tfunc (e {{.GoType}}) MarshalGQL(w io.Writer) {\n\t\tfmt.Fprint(w, strconv.Quote(e.String()))\n\t}\n\n{{- end }}\n", "object.gotpl": "{{ $object := . }}\n\nvar {{ $object.GQLType|lcFirst}}Implementors = {{$object.Implementors}}\n\n// nolint: gocyclo, errcheck, gas, goconst\n{{- if .Stream }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet) func() graphql.Marshaler {\n\tfields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors)\n\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\tObject: {{$object.GQLType|quote}},\n\t})\n\tif len(fields) != 1 {\n\t\tec.Errorf(ctx, \"must subscribe to exactly one stream\")\n\t\treturn nil\n\t}\n\n\tswitch fields[0].Name {\n\t{{- range $field := $object.Fields }}\n\tcase \"{{$field.GQLName}}\":\n\t\treturn ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, fields[0])\n\t{{- end }}\n\tdefault:\n\t\tpanic(\"unknown field \" + strconv.Quote(fields[0].Name))\n\t}\n}\n{{- else }}\nfunc (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel ast.SelectionSet{{if not $object.Root}}, obj *{{$object.FullName}} {{end}}) graphql.Marshaler {\n\tfields := graphql.CollectFields(ctx, sel, {{$object.GQLType|lcFirst}}Implementors)\n\t{{if $object.Root}}\n\t\tctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{\n\t\t\tObject: {{$object.GQLType|quote}},\n\t\t})\n\t{{end}}\n\tout := graphql.NewOrderedMap(len(fields))\n\tfor i, field := range fields {\n\t\tout.Keys[i] = field.Alias\n\n\t\tswitch field.Name {\n\t\tcase \"__typename\":\n\t\t\tout.Values[i] = graphql.MarshalString({{$object.GQLType|quote}})\n\t\t{{- range $field := $object.Fields }}\n\t\tcase \"{{$field.GQLName}}\":\n\t\t\tout.Values[i] = ec._{{$object.GQLType}}_{{$field.GQLName}}(ctx, field{{if not $object.Root}}, obj{{end}})\n\t\t{{- end }}\n\t\tdefault:\n\t\t\tpanic(\"unknown field \" + strconv.Quote(field.Name))\n\t\t}\n\t}\n\n\treturn out\n}\n{{- end }}\n", "resolver.gotpl": "//go:generate gorunpkg github.com/vektah/gqlgen\n\npackage {{ .PackageName }}\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\ntype {{.ResolverType}} struct {}\n\n{{ range $object := .Objects -}}\n\t{{- if $object.HasResolvers -}}\n\t\tfunc (r *{{$.ResolverType}}) {{$object.GQLType}}() {{ $object.ResolverInterface.FullName }} {\n\t\t\treturn &{{lcFirst $object.GQLType}}Resolver{r}\n\t\t}\n\t{{ end -}}\n{{ end }}\n\n{{ range $object := .Objects -}}\n\t{{- if $object.HasResolvers -}}\n\t\ttype {{lcFirst $object.GQLType}}Resolver struct { *Resolver }\n\n\t\t{{ range $field := $object.Fields -}}\n\t\t\t{{- if $field.IsResolver -}}\n\t\t\tfunc (r *{{lcFirst $object.GQLType}}Resolver) {{ $field.ShortResolverDeclaration }} {\n\t\t\t\tpanic(\"not implemented\")\n\t\t\t}\n\t\t\t{{ end -}}\n\t\t{{ end -}}\n\t{{ end -}}\n{{ end }}\n", + "server.gotpl": "package main\n\nimport (\n{{- range $import := .Imports }}\n\t{{- $import.Write }}\n{{ end }}\n)\n\nconst defaultPort = \"8080\"\n\nfunc main() {\n\tport := os.Getenv(\"PORT\")\n\tif port == \"\" {\n\t\tport = defaultPort\n\t}\n\n\thttp.Handle(\"/\", handler.Playground(\"GraphQL playground\", \"/query\"))\n\thttp.Handle(\"/query\", handler.GraphQL({{.ExecPackageName}}.NewExecutableSchema({{.ExecPackageName}}.Config{Resolvers: &{{.ResolverPackageName}}.Resolver{}})))\n\n\tlog.Printf(\"connect to http://localhost:%s/ for GraphQL playground\", port)\n\tlog.Fatal(http.ListenAndServe(\":\" + port, nil))\n}\n", } diff --git a/codegen/templates/server.gotpl b/codegen/templates/server.gotpl new file mode 100644 index 00000000000..f23b30e1166 --- /dev/null +++ b/codegen/templates/server.gotpl @@ -0,0 +1,22 @@ +package main + +import ( +{{- range $import := .Imports }} + {{- $import.Write }} +{{ end }} +) + +const defaultPort = "8080" + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = defaultPort + } + + http.Handle("/", handler.Playground("GraphQL playground", "/query")) + http.Handle("/query", handler.GraphQL({{.ExecPackageName}}.NewExecutableSchema({{.ExecPackageName}}.Config{Resolvers: &{{.ResolverPackageName}}.Resolver{}}))) + + log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) + log.Fatal(http.ListenAndServe(":" + port, nil)) +}