From c855272921a9d21a9eb321276b36a9ac65c4593e Mon Sep 17 00:00:00 2001 From: Robert McNeil Date: Tue, 21 Aug 2018 17:28:42 -0700 Subject: [PATCH 1/8] Put newline at end of gqlgen init output --- cmd/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/init.go b/cmd/init.go index fc709d6df4f..4b7792f6c2f 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -94,7 +94,7 @@ func GenerateGraphServer(config *codegen.Config) { os.Exit(1) } - fmt.Fprintf(os.Stdout, `Exec "go run ./%s" to start GraphQL server`, serverFilename) + fmt.Fprintf(os.Stdout, "Exec \"go run ./%s\" to start GraphQL server\n", serverFilename) } func initConfig() *codegen.Config { From 07ee49f3162553b41d45cee11ed0b96ecfe5d745 Mon Sep 17 00:00:00 2001 From: hollowj Date: Wed, 22 Aug 2018 15:41:43 +0300 Subject: [PATCH 2/8] Added args to introspection scheme directives. --- graphql/introspection/schema.go | 36 ++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/graphql/introspection/schema.go b/graphql/introspection/schema.go index dc93693b886..b5d2c482246 100644 --- a/graphql/introspection/schema.go +++ b/graphql/introspection/schema.go @@ -35,16 +35,34 @@ func (s *Schema) SubscriptionType() *Type { func (s *Schema) Directives() []Directive { var res []Directive + for _, d := range s.schema.Directives { - var locs []string - for _, loc := range d.Locations { - locs = append(locs, string(loc)) - } - res = append(res, Directive{ - Name: d.Name, - Description: d.Description, - Locations: locs, - }) + res = append(res, s.directiveFromDef(d)) } + return res } + +func (s *Schema) directiveFromDef(d *ast.DirectiveDefinition) Directive { + var locs []string + for _, loc := range d.Locations { + locs = append(locs, string(loc)) + } + + var args []InputValue + for _, arg := range d.Arguments { + args = append(args, InputValue{ + Name: arg.Name, + Description: arg.Description, + DefaultValue: defaultValue(arg.DefaultValue), + Type: WrapTypeFromType(s.schema, arg.Type), + }) + } + + return Directive{ + Name: d.Name, + Description: d.Description, + Locations: locs, + Args: args, + } +} From 12efa2d5d8c01a2359721b04a27a58c353567e34 Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Thu, 23 Aug 2018 11:15:03 +1000 Subject: [PATCH 3/8] add tests --- integration/generated.go | 39 ++++++++++++++++++++++++++--- integration/integration-test.js | 6 ++--- integration/schema-expected.graphql | 5 +++- integration/schema.graphql | 3 +++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/integration/generated.go b/integration/generated.go index 00a3ec341fd..b9795c39673 100644 --- a/integration/generated.go +++ b/integration/generated.go @@ -36,6 +36,7 @@ type ResolverRoot interface { } type DirectiveRoot struct { + Magic func(ctx context.Context, next graphql.Resolver, kind *int) (res interface{}, err error) } type ElementResolver interface { Child(ctx context.Context, obj *models.Element) (models.Element, error) @@ -1663,6 +1664,35 @@ func (ec *executionContext) FieldMiddleware(ctx context.Context, next graphql.Re ret = nil } }() + rctx := graphql.GetResolverContext(ctx) + for _, d := range rctx.Field.Definition.Directives { + switch d.Name { + case "magic": + if ec.directives.Magic != nil { + rawArgs := d.ArgumentMap(ec.Variables) + args := map[string]interface{}{} + var arg0 *int + if tmp, ok := rawArgs["kind"]; ok { + var err error + var ptr1 int + if tmp != nil { + ptr1, err = graphql.UnmarshalInt(tmp) + arg0 = &ptr1 + } + + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + } + args["kind"] = arg0 + n := next + next = func(ctx context.Context) (interface{}, error) { + return ec.directives.Magic(ctx, n, args["kind"].(*int)) + } + } + } + } res, err := ec.ResolverMiddleware(ctx, next) if err != nil { ec.Error(ctx, err) @@ -1680,7 +1710,10 @@ func (ec *executionContext) introspectType(name string) *introspection.Type { } var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `type Element { + &ast.Source{Name: "schema.graphql", Input: `"This directive does magical things" +directive @magic(kind: Int) on FIELD_DEFINITION + +type Element { child: Element! error: Boolean! mismatched: [Boolean!] @@ -1701,7 +1734,7 @@ enum DATE_FILTER_OP { input DateFilter { value: String! timezone: String = "UTC" - op: DATE_FILTER_OP = EQ + op: DATE_FILTER_OP = EQ @magic(kind: 1) } type User { @@ -1710,7 +1743,7 @@ type User { } type Viewer { - user: User + user: User @magic(kind: 1) } type Query { diff --git a/integration/integration-test.js b/integration/integration-test.js index 5880854d87c..e7bcc4871a6 100644 --- a/integration/integration-test.js +++ b/integration/integration-test.js @@ -4,12 +4,10 @@ import {ApolloClient} from "apollo-client"; import fetch from "node-fetch"; import gql from 'graphql-tag'; -if (!process.env.SERVER_URL) { - throw "SERVER_URL must be set" -} +var uri = process.env.SERVER_URL || 'http://localhost:8080/query'; const client = new ApolloClient({ - link: new HttpLink({uri: process.env.SERVER_URL, fetch: fetch}), + link: new HttpLink({uri, fetch}), cache: new InMemoryCache(), defaultOptions: { watchQuery: { diff --git a/integration/schema-expected.graphql b/integration/schema-expected.graphql index 212606aac20..52e5308faff 100644 --- a/integration/schema-expected.graphql +++ b/integration/schema-expected.graphql @@ -1,5 +1,8 @@ # source: http://localhost:8080/query -# timestamp: Fri Aug 10 2018 11:50:28 GMT+1000 (AEST) +# timestamp: Thu Aug 23 2018 10:30:37 GMT+1000 (AEST) + +"""This directive does magical things""" +directive @magic(kind: Int) on FIELD_DEFINITION enum DATE_FILTER_OP { EQ diff --git a/integration/schema.graphql b/integration/schema.graphql index 259aa40ad30..23c7d52aeec 100644 --- a/integration/schema.graphql +++ b/integration/schema.graphql @@ -1,3 +1,6 @@ +"This directive does magical things" +directive @magic(kind: Int) on FIELD_DEFINITION + type Element { child: Element! error: Boolean! From a81147dfed21e1725e5489ddfebfab6b4ea1bd7e Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Thu, 23 Aug 2018 11:52:09 +1000 Subject: [PATCH 4/8] Add a PR template --- .github/PULL_REQUEST_TEMPLATE.md | 5 ++++ TESTING.md | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 TESTING.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..0d87bf9a0f2 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +Describe your PR and link to any relevant issues. + +I have: + - [ ] Added tests covering the bug / feature (see [testing](https://github.com/99designs/gqlgen/blob/master/TESTING.md)) + - [ ] Updated any relevant documentation (see [docs](https://github.com/99designs/gqlgen/tree/master/docs/content)) diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000000..ad7e63352ac --- /dev/null +++ b/TESTING.md @@ -0,0 +1,40 @@ +How to write tests for gqlgen +=== + +Testing generated code is a little tricky, heres how its currently set up. + +### Testing responses from a server + +There is a server in `codegen/testserver` that is generated as part +of `go generate ./...`, and tests written against it. + +There are also a bunch of tests in against the examples, feel free to take examples from there. + + +### Testing the errors generated by the binary + +These tests are **really** slow, because they need to run the whole codegen step. Use them very sparingly. If you can, find a way to unit test it instead. + +Take a look at `codegen/input_test.go` for an example. + +### Testing introspection + +Introspection is tested by diffing the output of `graphql get-schema` against an expected output. + +Setting up the integration environment is a little tricky: +```bash +cd integration +go generate ./... +go run ./server/server.go +``` +in another terminal +```bash +cd integration +npm install +SERVER_URL=http://localhost:8080/query ./node_modules/.bin/graphql get-schema +``` + +will write the schema to `integration/schema-fetched.graphql`, compare that with `schema-expected.graphql` + +CI will run this and fail the build if the two files dont match. + From 64f3518edb7448bceaa4357a70f2e2a65fbc4c58 Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Thu, 23 Aug 2018 12:14:17 +1000 Subject: [PATCH 5/8] run generate --- integration/generated.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/generated.go b/integration/generated.go index b9795c39673..4b3b2546b19 100644 --- a/integration/generated.go +++ b/integration/generated.go @@ -1734,7 +1734,7 @@ enum DATE_FILTER_OP { input DateFilter { value: String! timezone: String = "UTC" - op: DATE_FILTER_OP = EQ @magic(kind: 1) + op: DATE_FILTER_OP = EQ } type User { @@ -1743,7 +1743,7 @@ type User { } type Viewer { - user: User @magic(kind: 1) + user: User } type Query { From 25b12cb600ae04866a089831c9fb4c01d2e53ab4 Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Thu, 23 Aug 2018 12:26:03 +1000 Subject: [PATCH 6/8] Remove 404s from sitemap --- docs/content/introduction.md | 1 + docs/layouts/sitemap.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 docs/layouts/sitemap.xml diff --git a/docs/content/introduction.md b/docs/content/introduction.md index 21c538d0836..2891ceec273 100644 --- a/docs/content/introduction.md +++ b/docs/content/introduction.md @@ -1,6 +1,7 @@ --- linkTitle: Introduction title: Type-safe graphql for golang +description: Generating GraphQL servers in golang with type safety type: homepage date: 2018-03-17T13:06:47+11:00 --- diff --git a/docs/layouts/sitemap.xml b/docs/layouts/sitemap.xml new file mode 100644 index 00000000000..b5a19b0107b --- /dev/null +++ b/docs/layouts/sitemap.xml @@ -0,0 +1,13 @@ + + {{ range .Data.Pages }} + {{ if or .Description (eq .Kind "home") }} + + {{ .Permalink }} + {{ if not .Lastmod.IsZero }}{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }} + {{ with .Sitemap.ChangeFreq }}{{ . }}{{ end }} + {{ if ge .Sitemap.Priority 0.0 }}{{ .Sitemap.Priority }}{{ end }} + + {{ end }} + {{ end }} + + From b082227dc3f43337ea17c0a1944e0e7397c31e0f Mon Sep 17 00:00:00 2001 From: jekaspekas Date: Thu, 23 Aug 2018 10:37:08 +0700 Subject: [PATCH 7/8] fix mapstructure unit test error fix unit test error "mapstructure: result must be a pointer". It appears instead of resolver returned error. --- client/websocket.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/websocket.go b/client/websocket.go index e4e06051339..46f402d376d 100644 --- a/client/websocket.go +++ b/client/websocket.go @@ -84,7 +84,7 @@ func (p *Client) Websocket(query string, options ...Option) *Subscription { if respDataRaw["errors"] != nil { var errs []*gqlerror.Error - if err = unpack(respDataRaw["errors"], errs); err != nil { + if err = unpack(respDataRaw["errors"], &errs); err != nil { return err } if len(errs) > 0 { From b07736ef8cfd03ba2a649c70cf8bfa3667102ecc Mon Sep 17 00:00:00 2001 From: Adam Scarr Date: Thu, 23 Aug 2018 15:27:07 +1000 Subject: [PATCH 8/8] Validate gopath when running gqlgen --- cmd/root.go | 11 ++++++ codegen/config.go | 16 ++------- codegen/config_test.go | 26 -------------- codegen/import_build.go | 10 ++---- internal/gopath/gopath.go | 37 ++++++++++++++++++++ internal/gopath/gopath_test.go | 62 ++++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 46 deletions(-) create mode 100644 internal/gopath/gopath.go create mode 100644 internal/gopath/gopath_test.go diff --git a/cmd/root.go b/cmd/root.go index ddd02f18b40..8598acd315c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "log" "os" + "github.com/99designs/gqlgen/internal/gopath" "github.com/spf13/cobra" ) @@ -39,6 +40,16 @@ var rootCmd = &cobra.Command{ Long: `This is a library for quickly creating strictly typed graphql servers in golang. See https://gqlgen.com/ for a getting started guide.`, PersistentPreRun: func(cmd *cobra.Command, args []string) { + pwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "unable to determine current workding dir: %s\n", err.Error()) + os.Exit(1) + } + + if !gopath.Contains(pwd) { + fmt.Fprintf(os.Stderr, "gqlgen must be run from inside your $GOPATH\n") + os.Exit(1) + } if verbose { log.SetFlags(0) } else { diff --git a/codegen/config.go b/codegen/config.go index 0ba1f65dabc..8e2a5e74b31 100644 --- a/codegen/config.go +++ b/codegen/config.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/99designs/gqlgen/internal/gopath" "github.com/pkg/errors" "github.com/vektah/gqlparser/ast" "gopkg.in/yaml.v2" @@ -107,22 +108,11 @@ func (c *PackageConfig) normalize() error { } func (c *PackageConfig) ImportPath() string { - dir := filepath.ToSlash(c.Dir()) - for _, gopath := range filepath.SplitList(build.Default.GOPATH) { - gopath = filepath.ToSlash(gopath) + "/src/" - if len(gopath) > len(dir) { - continue - } - if strings.EqualFold(gopath, dir[0:len(gopath)]) { - dir = dir[len(gopath):] - break - } - } - return dir + return gopath.MustDir2Import(c.Dir()) } func (c *PackageConfig) Dir() string { - return filepath.ToSlash(filepath.Dir(c.Filename)) + return filepath.Dir(c.Filename) } func (c *PackageConfig) Check() error { diff --git a/codegen/config_test.go b/codegen/config_test.go index f4260d6dfb7..f990decf44b 100644 --- a/codegen/config_test.go +++ b/codegen/config_test.go @@ -1,12 +1,10 @@ package codegen import ( - "go/build" "os" "path/filepath" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -58,27 +56,3 @@ func TestLoadDefaultConfig(t *testing.T) { require.True(t, os.IsNotExist(err)) }) } - -func Test_fullPackageName(t *testing.T) { - origBuildContext := build.Default - defer func() { build.Default = origBuildContext }() - - t.Run("gopath longer than package name", func(t *testing.T) { - p := PackageConfig{Filename: "/b/src/y/foo/bar/baz.go"} - build.Default.GOPATH = "/a/src/xxxxxxxxxxxxxxxxxxxxxxxx:/b/src/y" - var got string - ok := assert.NotPanics(t, func() { got = p.ImportPath() }) - if ok { - assert.Equal(t, "/b/src/y/foo/bar", got) - } - }) - t.Run("stop searching on first hit", func(t *testing.T) { - p := PackageConfig{Filename: "/a/src/x/foo/bar/baz.go"} - build.Default.GOPATH = "/a/src/x:/b/src/y" - var got string - ok := assert.NotPanics(t, func() { got = p.ImportPath() }) - if ok { - assert.Equal(t, "/a/src/x/foo/bar", got) - } - }) -} diff --git a/codegen/import_build.go b/codegen/import_build.go index d7d96774454..d8f2a2def22 100644 --- a/codegen/import_build.go +++ b/codegen/import_build.go @@ -5,12 +5,11 @@ import ( "go/build" "sort" "strconv" - "strings" - // Import and ignore the ambient imports listed below so dependency managers // don't prune unused code for us. Both lists should be kept in sync. _ "github.com/99designs/gqlgen/graphql" _ "github.com/99designs/gqlgen/graphql/introspection" + "github.com/99designs/gqlgen/internal/gopath" _ "github.com/vektah/gqlparser" _ "github.com/vektah/gqlparser/ast" ) @@ -55,7 +54,8 @@ func (s *Imports) add(path string) *Import { return nil } - if stringHasSuffixFold(s.destDir, path) { + // if we are referencing our own package we dont need an import + if gopath.MustDir2Import(s.destDir) == path { return nil } @@ -77,10 +77,6 @@ func (s *Imports) add(path string) *Import { return imp } -func stringHasSuffixFold(s, suffix string) bool { - return len(s) >= len(suffix) && strings.EqualFold(s[len(s)-len(suffix):], suffix) -} - func (s Imports) finalize() []*Import { // ensure stable ordering by sorting sort.Slice(s.imports, func(i, j int) bool { diff --git a/internal/gopath/gopath.go b/internal/gopath/gopath.go new file mode 100644 index 00000000000..c9b66167a3e --- /dev/null +++ b/internal/gopath/gopath.go @@ -0,0 +1,37 @@ +package gopath + +import ( + "fmt" + "go/build" + "path/filepath" + "strings" +) + +var NotFound = fmt.Errorf("not on GOPATH") + +// Contains returns true if the given directory is in the GOPATH +func Contains(dir string) bool { + _, err := Dir2Import(dir) + return err == nil +} + +// Dir2Import takes an *absolute* path and returns a golang import path for the package, and returns an error if it isn't on the gopath +func Dir2Import(dir string) (string, error) { + dir = filepath.ToSlash(dir) + for _, gopath := range filepath.SplitList(build.Default.GOPATH) { + gopath = filepath.ToSlash(filepath.Join(gopath, "src")) + if len(gopath) < len(dir) && strings.EqualFold(gopath, dir[0:len(gopath)]) { + return dir[len(gopath)+1:], nil + } + } + return "", NotFound +} + +// MustDir2Import takes an *absolute* path and returns a golang import path for the package, and panics if it isn't on the gopath +func MustDir2Import(dir string) string { + pkg, err := Dir2Import(dir) + if err != nil { + panic(err) + } + return pkg +} diff --git a/internal/gopath/gopath_test.go b/internal/gopath/gopath_test.go new file mode 100644 index 00000000000..847ad1e8567 --- /dev/null +++ b/internal/gopath/gopath_test.go @@ -0,0 +1,62 @@ +package gopath + +import ( + "go/build" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContains(t *testing.T) { + origBuildContext := build.Default + defer func() { build.Default = origBuildContext }() + + if runtime.GOOS == "windows" { + build.Default.GOPATH = `C:\go;C:\Users\user\go` + + assert.True(t, Contains(`C:\go\src\github.com\vektah\gqlgen`)) + assert.True(t, Contains(`C:\go\src\fpp`)) + assert.True(t, Contains(`C:/go/src/github.com/vektah/gqlgen`)) + assert.True(t, Contains(`C:\Users\user\go\src\foo`)) + assert.False(t, Contains(`C:\tmp`)) + assert.False(t, Contains(`C:\Users\user`)) + assert.False(t, Contains(`C:\Users\another\go`)) + } else { + build.Default.GOPATH = "/go:/home/user/go" + + assert.True(t, Contains("/go/src/github.com/vektah/gqlgen")) + assert.True(t, Contains("/go/src/foo")) + assert.True(t, Contains("/home/user/go/src/foo")) + assert.False(t, Contains("/tmp")) + assert.False(t, Contains("/home/user")) + assert.False(t, Contains("/home/another/go")) + } +} + +func TestDir2Package(t *testing.T) { + origBuildContext := build.Default + defer func() { build.Default = origBuildContext }() + + if runtime.GOOS == "windows" { + build.Default.GOPATH = "C:/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;C:/a/y;C:/b/" + + assert.Equal(t, "foo/bar", MustDir2Import("C:/a/y/src/foo/bar")) + assert.Equal(t, "foo/bar", MustDir2Import(`C:\a\y\src\foo\bar`)) + assert.Equal(t, "foo/bar", MustDir2Import("C:/b/src/foo/bar")) + assert.Equal(t, "foo/bar", MustDir2Import(`C:\b\src\foo\bar`)) + + assert.PanicsWithValue(t, NotFound, func() { + MustDir2Import("C:/tmp/foo") + }) + } else { + build.Default.GOPATH = "/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:/a/y:/b/" + + assert.Equal(t, "foo/bar", MustDir2Import("/a/y/src/foo/bar")) + assert.Equal(t, "foo/bar", MustDir2Import("/b/src/foo/bar")) + + assert.PanicsWithValue(t, NotFound, func() { + MustDir2Import("/tmp/foo") + }) + } +}