Skip to content

Commit

Permalink
Infer entry point types, and improve error messages
Browse files Browse the repository at this point in the history
Consistent with [schema parsing behavior from graphql-js][1], this
commit allows graphql-go to infer entry point types without an explicit
`schema` declaration if there are types in the schema named "Query",
"Mutation", or "Subscription".

It also returns a more descriptive error message if no query type is
declared, which fixes graph-gophers#125. Previously if no `schema` declaration was
present a cryptic panic would be thrown at runtime. (`interface
conversion: resolvable.Resolvable is nil, not *resolvable.Object`)

[1]: https://github.com/graphql/graphql-js/blob/master@{2017-11-18}/src/utilities/buildASTSchema.js#L167-L221
  • Loading branch information
vergenzt committed Nov 19, 2017
1 parent beff084 commit 28d7234
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 28 deletions.
82 changes: 82 additions & 0 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ func TestHelloWorld(t *testing.T) {
})
}

func TestHelloWithoutSchemaDecl(t *testing.T) {
gqltesting.RunTests(t, []*gqltesting.Test{
{
Schema: graphql.MustParseSchema(`
type Query {
hello: String!
}
`, &helloWorldResolver1{}),
Query: `
{
hello
}
`,
ExpectedResult: `
{
"hello": "Hello world!"
}
`,
},
})
}

func TestHelloSnake(t *testing.T) {
gqltesting.RunTests(t, []*gqltesting.Test{
{
Expand Down Expand Up @@ -207,6 +229,66 @@ func TestHelloSnakeArguments(t *testing.T) {
})
}

func TestNoSchemaOrQueryDeclaration(t *testing.T) {
_, got := graphql.ParseSchema(`
type Foo {
someField: String!
}
`, nil)
want := `graphql: must provide "schema" definition with "query" type or a type named "Query"`
if got == nil || got.Error() != want {
t.Logf("got: %s", got)
t.Logf("want: %s", want)
t.Fail()
}
}

func TestOnlyMutationDeclaration(t *testing.T) {
_, got := graphql.ParseSchema(`
schema {
mutation: Mutation
}
type Mutation {
test: Boolean!
}
`, nil)
want := `graphql: must provide "schema" definition with "query" type or a type named "Query"`
if got == nil || got.Error() != want {
t.Logf("got: %s", got)
t.Logf("want: %s", want)
t.Fail()
}
}

func TestOnlyQueryAndMutationDeclaration(t *testing.T) {
gqltesting.RunTests(t, []*gqltesting.Test{
{
Schema: graphql.MustParseSchema(`
type Query {
theNumber: Int!
}
type Mutation {
changeTheNumber(newNumber: Int!): Query
}
`, &theNumberResolver{}),
Query: `
mutation {
changeTheNumber(newNumber: 1) {
theNumber
}
}
`,
ExpectedResult: `
{
"changeTheNumber": {
"theNumber": 1
}
}
`,
},
})
}

func TestBasic(t *testing.T) {
gqltesting.RunTests(t, []*gqltesting.Test{
{
Expand Down
3 changes: 1 addition & 2 deletions internal/schema/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ package schema
var Meta *Schema

func init() {
Meta = &Schema{} // bootstrap
Meta = New()
if err := Meta.Parse(metaSrc); err != nil {
if err := Meta.parseWithoutEntryPoints(metaSrc); err != nil {
panic(err)
}
}
Expand Down
93 changes: 67 additions & 26 deletions internal/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,20 +141,55 @@ type Field struct {

func New() *Schema {
s := &Schema{
entryPointNames: make(map[string]string),
Types: make(map[string]NamedType),
Directives: make(map[string]*DirectiveDecl),
Types: make(map[string]NamedType),
Directives: make(map[string]*DirectiveDecl),
}
for n, t := range Meta.Types {
s.Types[n] = t
}
for n, d := range Meta.Directives {
s.Directives[n] = d
if Meta != nil {
for n, t := range Meta.Types {
s.Types[n] = t
}
for n, d := range Meta.Directives {
s.Directives[n] = d
}
}
return s
}

func (s *Schema) Parse(schemaString string) error {
err := s.parseWithoutEntryPoints(schemaString)
if err != nil {
return err
}

s.EntryPoints = make(map[string]NamedType)
if s.entryPointNames != nil {
for key, name := range s.entryPointNames {
t, ok := s.Types[name]
if !ok {
return errors.Errorf("type %q not found", name)
}
s.EntryPoints[key] = t
}
} else {
if queryType, ok := s.Types["Query"]; ok {
s.EntryPoints["query"] = queryType
}
if mutationType, ok := s.Types["Mutation"]; ok {
s.EntryPoints["mutation"] = mutationType
}
if subscriptionType, ok := s.Types["Subscription"]; ok {
s.EntryPoints["subscription"] = subscriptionType
}
}

if _, hasQueryType := s.EntryPoints["query"]; !hasQueryType {
return errors.Errorf(`must provide "schema" definition with "query" type or a type named "Query"`)
}

return nil
}

func (s *Schema) parseWithoutEntryPoints(schemaString string) error {
sc := &scanner.Scanner{
Mode: scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings,
}
Expand Down Expand Up @@ -183,17 +218,6 @@ func (s *Schema) Parse(schemaString string) error {
}
}

s.EntryPoints = make(map[string]NamedType)
for key, name := range s.entryPointNames {
t, ok := s.Types[name]
if !ok {
if !ok {
return errors.Errorf("type %q not found", name)
}
}
s.EntryPoints[key] = t
}

for _, obj := range s.objects {
obj.Interfaces = make([]*Interface, len(obj.interfaceNames))
for i, intfName := range obj.interfaceNames {
Expand Down Expand Up @@ -307,14 +331,10 @@ func parseSchema(s *Schema, l *common.Lexer) {
desc := l.DescComment()
switch x := l.ConsumeIdent(); x {
case "schema":
l.ConsumeToken('{')
for l.Peek() != '}' {
name := l.ConsumeIdent()
l.ConsumeToken(':')
typ := l.ConsumeIdent()
s.entryPointNames[name] = typ
if s.entryPointNames != nil {
l.SyntaxError(fmt.Sprintf(`cannot declare "schema" more than once`))
}
l.ConsumeToken('}')
s.entryPointNames = parseSchemaDecl(l)
case "type":
obj := parseObjectDecl(l)
obj.Desc = desc
Expand Down Expand Up @@ -351,6 +371,27 @@ func parseSchema(s *Schema, l *common.Lexer) {
}
}

func parseSchemaDecl(l *common.Lexer) map[string]string {
entryPointNames := make(map[string]string)
l.ConsumeToken('{')
for l.Peek() != '}' {
name := l.ConsumeIdent()
l.ConsumeToken(':')
typ := l.ConsumeIdent()
switch name {
case "query", "mutation", "subscription":
if _, alreadyDeclared := entryPointNames["name"]; alreadyDeclared {
l.SyntaxError(fmt.Sprintf(`cannot declare %q more than once`, name))
}
entryPointNames[name] = typ
default:
l.SyntaxError(fmt.Sprintf(`unexpected %q, expecting "query", "mutation", or "subscription"`, name))
}
}
l.ConsumeToken('}')
return entryPointNames
}

func parseObjectDecl(l *common.Lexer) *Object {
o := &Object{}
o.Name = l.ConsumeIdent()
Expand Down
1 change: 1 addition & 0 deletions internal/tests/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestAll(t *testing.T) {
for i, schemaStr := range testData.Schemas {
schemas[i] = schema.New()
if err := schemas[i].Parse(schemaStr); err != nil {
t.Logf("\n" + schemaStr)
t.Fatal(err)
}
}
Expand Down

0 comments on commit 28d7234

Please sign in to comment.