Repository: graph-gophers/graphql-go (go)

Issue:
BlockStringValues do not strip common indentation
Multiline string (Block String) support leaves common leading indentation on each line, resulting in unexpected indentation in documentation. This has impacts such as incorrectly formatting when rendered in CommonMark (like in GraphiQL), as subsequent lines after the first end up being treated as code blocks.

The spec calls out the need to strip this leading indentation: https://graphql.github.io/graphql-spec/June2018/#sec-String-Value

> Since block strings represent freeform text often used in indented positions, the string value semantics of a block string excludes uniform indentation and blank initial and trailing lines via BlockStringValue().

https://github.com/graphql/graphql-js implements this, and refers to that section of the spec: https://github.com/graphql/graphql-js/blob/master/src/language/blockString.js#L3:L34

I have verified this behaviour, comparing `graphql-js` and `graphql-go` for this basic schema:

```
  schema {
     query: Query
  }
  type Query {
    """
    First line of docs.

    Second line of docs.

        ... code example ...
        ... code example ...
        ... code example ...
    """
    hello: String!
  }
```

For the `hello` field `graphql-js`, the introspection query returns the `description` value as:

```
First line of docs.\n\nSecond line of docs.\n\n    ... code example ...\n    ... code example ...\n    ... code example ...
```

While `graphql-go` returns:

```
First line of docs.\n\n    Second line of docs.\n\n        ... code example ...\n        ... code example ...\n        ... code example ...
```

Pull request description:
Strip Common Indentation from BlockString Descriptions
Fixes #355 

Patch:
diff --git a/README.md b/README.md
index be58951..01038bf 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,9 @@ func (_ *query) Hello() string { return "Hello, world!" }
 
 func main() {
         s := `
+                schema {
+                        query: Query
+                }
                 type Query {
                         hello: String!
                 }
@@ -100,57 +103,6 @@ func (r *helloWorldResolver) Hello(ctx context.Context) (string, error) {
 }
 ```
 
-### Custom Errors
-
-Errors returned by resolvers can include custom extensions by implementing the `ResolverError` interface:
-
-```
-type ResolverError interface {
-	error
-	Extensions() map[string]interface{}
-}
-```
-
-Example of a simple custom error:
-
-```
-type droidNotFoundError struct {
-	Code    string `json:"code"`
-	Message string `json:"message"`
-}
-
-func (e droidNotFoundError) Error() string {
-	return fmt.Sprintf("error [%s]: %s", e.Code, e.Message)
-}
-
-func (e droidNotFoundError) Extensions() map[string]interface{} {
-	return map[string]interface{}{
-		"code":    e.Code,
-		"message": e.Message,
-	}
-}
-```
-
-Which could produce a GraphQL error such as:
-
-```
-{
-  "errors": [
-    {
-      "message": "error [NotFound]: This is not the droid you are looking for",
-      "path": [
-        "droid"
-      ],
-      "extensions": {
-        "code": "NotFound",
-        "message": "This is not the droid you are looking for"
-      }
-    }
-  ],
-  "data": null
-}
-```
-
 ### Community Examples
 
 [tonyghita/graphql-go-example](https://github.com/tonyghita/graphql-go-example) - A more "productionized" version of the Star Wars API example given in this repository.
diff --git a/example/customerrors/server/server.go b/example/customerrors/server/server.go
deleted file mode 100644
index 7a94a05..0000000
--- a/example/customerrors/server/server.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package main
-
-import (
-	"log"
-	"net/http"
-
-	"github.com/graph-gophers/graphql-go"
-	"github.com/graph-gophers/graphql-go/example/customerrors"
-	"github.com/graph-gophers/graphql-go/relay"
-)
-
-var schema *graphql.Schema
-
-func init() {
-	schema = graphql.MustParseSchema(customerrors.Schema, &customerrors.Resolver{})
-}
-
-func main() {
-	http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.Write(page)
-	}))
-
-	http.Handle("/query", &relay.Handler{Schema: schema})
-
-	log.Fatal(http.ListenAndServe(":8080", nil))
-}
-
-var page = []byte(`
-<!DOCTYPE html>
-<html>
-	<head>
-		<link href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css" rel="stylesheet" />
-		<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
-		<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
-		<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
-		<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
-		<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
-	</head>
-	<body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
-		<div id="graphiql" style="height: 100vh;">Loading...</div>
-		<script>
-			function graphQLFetcher(graphQLParams) {
-				return fetch("/query", {
-					method: "post",
-					body: JSON.stringify(graphQLParams),
-					credentials: "include",
-				}).then(function (response) {
-					return response.text();
-				}).then(function (responseBody) {
-					try {
-						return JSON.parse(responseBody);
-					} catch (error) {
-						return responseBody;
-					}
-				});
-			}
-
-			ReactDOM.render(
-				React.createElement(GraphiQL, {fetcher: graphQLFetcher}),
-				document.getElementById("graphiql")
-			);
-		</script>
-	</body>
-</html>
-`)
diff --git a/example/customerrors/starwars.go b/example/customerrors/starwars.go
deleted file mode 100644
index 086d65c..0000000
--- a/example/customerrors/starwars.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package customerrors
-
-import (
-	"fmt"
-
-	"github.com/graph-gophers/graphql-go"
-)
-
-var Schema = `
-	schema {
-		query: Query
-	}
-	type Query {
-		droid(id: ID!): Droid!
-	}
-	# An autonomous mechanical character in the Star Wars universe
-	type Droid {
-		# The ID of the droid
-		id: ID!
-		# What others call this droid
-		name: String!
-	}
-`
-
-type droid struct {
-	ID   graphql.ID
-	Name string
-}
-
-var droids = []*droid{
-	{ID: "2000", Name: "C-3PO"},
-	{ID: "2001", Name: "R2-D2"},
-}
-
-var droidData = make(map[graphql.ID]*droid)
-
-func init() {
-	for _, d := range droids {
-		droidData[d.ID] = d
-	}
-}
-
-type Resolver struct{}
-
-func (r *Resolver) Droid(args struct{ ID graphql.ID }) (*droidResolver, error) {
-	if d := droidData[args.ID]; d != nil {
-		return &droidResolver{d: d}, nil
-	}
-	return nil, &droidNotFoundError{Code: "NotFound", Message: "This is not the droid you are looking for"}
-}
-
-type droidResolver struct {
-	d *droid
-}
-
-func (r *droidResolver) ID() graphql.ID {
-	return r.d.ID
-}
-
-func (r *droidResolver) Name() string {
-	return r.d.Name
-}
-
-type droidNotFoundError struct {
-	Code    string `json:"code"`
-	Message string `json:"message"`
-}
-
-func (e droidNotFoundError) Error() string {
-	return fmt.Sprintf("error [%s]: %s", e.Code, e.Message)
-}
-
-func (e droidNotFoundError) Extensions() map[string]interface{} {
-	return map[string]interface{}{
-		"code":    e.Code,
-		"message": e.Message,
-	}
-}
diff --git a/graphql.go b/graphql.go
index 0a66179..b99b504 100644
--- a/graphql.go
+++ b/graphql.go
@@ -37,9 +37,6 @@ func ParseSchema(schemaString string, resolver interface{}, opts ...SchemaOpt) (
 	if err := s.schema.Parse(schemaString, s.useStringDescriptions); err != nil {
 		return nil, err
 	}
-	if err := s.validateSchema(); err != nil {
-		return nil, err
-	}
 
 	r, err := resolvable.ApplyResolver(s.schema, resolver)
 	if err != nil {
@@ -186,11 +183,6 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str
 	if op.Type == query.Subscription {
 		return &Response{Errors: []*errors.QueryError{&errors.QueryError{ Message: "graphql-ws protocol header is missing" }}}
 	}
-	if op.Type == query.Mutation {
-		if _, ok := s.schema.EntryPoints["mutation"]; !ok {
-			return &Response{Errors: []*errors.QueryError{{ Message: "no mutations are offered by the schema" }}}
-		}
-	}
 
 	// Fill in variables with the defaults from the operation
 	if variables == nil {
@@ -231,39 +223,6 @@ func (s *Schema) exec(ctx context.Context, queryString string, operationName str
 	}
 }
 
-func (s *Schema) validateSchema() error {
-	// https://graphql.github.io/graphql-spec/June2018/#sec-Root-Operation-Types
-	// > The query root operation type must be provided and must be an Object type.
-	if err := validateRootOp(s.schema, "query", true); err != nil {
-		return err
-	}
-	// > The mutation root operation type is optional; if it is not provided, the service does not support mutations.
-	// > If it is provided, it must be an Object type.
-	if err := validateRootOp(s.schema, "mutation", false); err != nil {
-		return err
-	}
-	// > Similarly, the subscription root operation type is also optional; if it is not provided, the service does not
-	// > support subscriptions. If it is provided, it must be an Object type.
-	if err := validateRootOp(s.schema, "subscription", false); err != nil {
-		return err
-	}
-	return nil
-}
-
-func validateRootOp(s *schema.Schema, name string, mandatory bool) error {
-	t, ok := s.EntryPoints[name]
-	if !ok {
-		if mandatory {
-			return fmt.Errorf("root operation %q must be defined", name)
-		}
-		return nil
-	}
-	if t.Kind() != "OBJECT" {
-		return fmt.Errorf("root operation %q must be an OBJECT", name)
-	}
-	return nil
-}
-
 func getOperation(document *query.Document, operationName string) (*query.Operation, error) {
 	if len(document.Operations) == 0 {
 		return nil, fmt.Errorf("no operations in query document")
diff --git a/internal/common/blockstring.go b/internal/common/blockstring.go
new file mode 100644
index 0000000..1f7fe81
--- /dev/null
+++ b/internal/common/blockstring.go
@@ -0,0 +1,103 @@
+// MIT License
+//
+// Copyright (c) 2019 GraphQL Contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+// This implementation has been adapted from the graphql-js reference implementation
+// https://github.com/graphql/graphql-js/blob/5eb7c4ded7ceb83ac742149cbe0dae07a8af9a30/src/language/blockString.js
+// which is released under the MIT License above.
+
+package common
+
+import (
+	"strings"
+)
+
+// Produces the value of a block string from its parsed raw value, similar to
+// CoffeeScript's block string, Python's docstring trim or Ruby's strip_heredoc.
+//
+// This implements the GraphQL spec's BlockStringValue() static algorithm.
+func blockString(raw string) string {
+	lines := strings.Split(raw, "\n")
+
+	// Remove common indentation from all lines except the first (which has none)
+	ind := blockStringIndentation(lines)
+	if ind > 0 {
+		for i := 1; i < len(lines); i++ {
+			l := lines[i]
+			if len(l) < ind {
+				lines[i] = ""
+				continue
+			}
+			lines[i] = l[ind:]
+		}
+	}
+
+	// Remove leading and trailing blank lines
+	trimStart := 0
+	for i := 0; i < len(lines) && isBlank(lines[i]); i++ {
+		trimStart++
+	}
+	lines = lines[trimStart:]
+	trimEnd := 0
+	for i := len(lines) - 1; i > 0 && isBlank(lines[i]); i-- {
+		trimEnd++
+	}
+	lines = lines[:len(lines)-trimEnd]
+
+	return strings.Join(lines, "\n")
+}
+
+func blockStringIndentation(lines []string) int {
+	var commonIndent *int
+	for i := 1; i < len(lines); i++ {
+		l := lines[i]
+		indent := leadingWhitespace(l)
+		if indent == len(l) {
+			// don't consider blank/empty lines
+			continue
+		}
+		if indent == 0 {
+			return 0
+		}
+		if commonIndent == nil || indent < *commonIndent {
+			commonIndent = &indent
+		}
+	}
+	if commonIndent == nil {
+		return 0
+	}
+	return *commonIndent
+}
+
+func isBlank(s string) bool {
+	return len(s) == 0 || leadingWhitespace(s) == len(s)
+}
+
+func leadingWhitespace(s string) int {
+	i := 0
+	for _, r := range s {
+		if r != '\t' && r != ' ' {
+			break
+		}
+		i++
+	}
+	return i
+}
diff --git a/internal/common/lexer.go b/internal/common/lexer.go
index 9cc7e54..5807f7a 100644
--- a/internal/common/lexer.go
+++ b/internal/common/lexer.go
@@ -184,8 +184,7 @@ func (l *Lexer) consumeTripleQuoteComment() string {
 	}
 	val := buf.String()
 	val = val[:len(val)-numQuotes]
-	val = strings.TrimSpace(val)
-	return val
+	return blockString(val)
 }
 
 func (l *Lexer) consumeStringComment() string {
diff --git a/internal/exec/packer/packer.go b/internal/exec/packer/packer.go
index fca88da..22706bc 100644
--- a/internal/exec/packer/packer.go
+++ b/internal/exec/packer/packer.go
@@ -160,7 +160,7 @@ func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Typ
 		usePtr = true
 	}
 	if structType.Kind() != reflect.Struct {
-		return nil, fmt.Errorf("expected struct or pointer to struct, got %s (hint: missing `args struct { ... }` wrapper for field arguments?)", typ)
+		return nil, fmt.Errorf("expected struct or pointer to struct, got %s", typ)
 	}
 
 	var fields []*structPackerField
@@ -172,7 +172,7 @@ func (b *Builder) MakeStructPacker(values common.InputValueList, typ reflect.Typ
 
 		sf, ok := structType.FieldByNameFunc(fx)
 		if !ok {
-			return nil, fmt.Errorf("%s does not define field %q (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)", typ, v.Name.Name)
+			return nil, fmt.Errorf("missing argument %q", v.Name)
 		}
 		if sf.PkgPath != "" {
 			return nil, fmt.Errorf("field %q must be exported", sf.Name)
diff --git a/internal/exec/resolvable/resolvable.go b/internal/exec/resolvable/resolvable.go
index a3a5048..1b248e6 100644
--- a/internal/exec/resolvable/resolvable.go
+++ b/internal/exec/resolvable/resolvable.go
@@ -255,7 +255,7 @@ func (b *execBuilder) makeObjectExec(typeName string, fields schema.FieldList, p
 		}
 		fe, err := b.makeFieldExec(typeName, f, m, sf, methodIndex, fieldIndex, methodHasReceiver)
 		if err != nil {
-			return nil, fmt.Errorf("%s\n\tused by (%s).%s", err, resolverType, m.Name)
+			return nil, fmt.Errorf("%s\n\treturned by (%s).%s", err, resolverType, m.Name)
 		}
 		Fields[f.Name] = fe
 	}
@@ -362,8 +362,7 @@ func (b *execBuilder) makeFieldExec(typeName string, f *schema.Field, m reflect.
 	var out reflect.Type
 	if methodIndex != -1 {
 		out = m.Type.Out(0)
-		sub, ok := b.schema.EntryPoints["subscription"]
-		if ok && typeName == sub.TypeName() && out.Kind() == reflect.Chan {
+		if typeName == "Subscription" && out.Kind() == reflect.Chan {
 			out = m.Type.Out(0).Elem()
 		}
 	} else {
diff --git a/internal/schema/schema.go b/internal/schema/schema.go
index cc7ebe4..dd9a16c 100644
--- a/internal/schema/schema.go
+++ b/internal/schema/schema.go
@@ -286,21 +286,6 @@ func (s *Schema) Parse(schemaString string, useStringDescriptions bool) error {
 		}
 	}
 
-	// https://graphql.github.io/graphql-spec/June2018/#sec-Root-Operation-Types
-	// > While any type can be the root operation type for a GraphQL operation, the type system definition language can
-	// > omit the schema definition when the query, mutation, and subscription root types are named Query, Mutation,
-	// > and Subscription respectively.
-	if len(s.entryPointNames) == 0 {
-		if _, ok := s.Types["Query"]; ok {
-			s.entryPointNames["query"] = "Query"
-		}
-		if _, ok := s.Types["Mutation"]; ok {
-			s.entryPointNames["mutation"] = "Mutation"
-		}
-		if _, ok := s.Types["Subscription"]; ok {
-			s.entryPointNames["subscription"] = "Subscription"
-		}
-	}
 	s.EntryPoints = make(map[string]NamedType)
 	for key, name := range s.entryPointNames {
 		t, ok := s.Types[name]
diff --git a/log/log.go b/log/log.go
index bdada87..25569af 100644
--- a/log/log.go
+++ b/log/log.go
@@ -15,9 +15,9 @@ type Logger interface {
 type DefaultLogger struct{}
 
 // LogPanic is used to log recovered panic values that occur during query execution
-func (l *DefaultLogger) LogPanic(ctx context.Context, value interface{}) {
+func (l *DefaultLogger) LogPanic(_ context.Context, value interface{}) {
 	const size = 64 << 10
 	buf := make([]byte, size)
 	buf = buf[:runtime.Stack(buf, false)]
-	log.Printf("graphql: panic occurred: %v\n%s\ncontext: %v", value, buf, ctx)
+	log.Printf("graphql: panic occurred: %v\n%s", value, buf)
 }
diff --git a/subscriptions.go b/subscriptions.go
index 4199c06..2ee71e5 100644
--- a/subscriptions.go
+++ b/subscriptions.go
@@ -24,9 +24,6 @@ func (s *Schema) Subscribe(ctx context.Context, queryString string, operationNam
 	if s.res.Resolver == (reflect.Value{}) {
 		return nil, errors.New("schema created without resolver, can not subscribe")
 	}
-	if _, ok := s.schema.EntryPoints["subscription"]; !ok {
-		return nil, errors.New("no subscriptions are offered by the schema")
-	}
 	return s.subscribe(ctx, queryString, operationName, variables, s.res), nil
 }
 
diff --git a/time.go b/time.go
index 5e80d42..829c502 100644
--- a/time.go
+++ b/time.go
@@ -31,12 +31,9 @@ func (t *Time) UnmarshalGraphQL(input interface{}) error {
 		var err error
 		t.Time, err = time.Parse(time.RFC3339, input)
 		return err
-	case int32:
+	case int:
 		t.Time = time.Unix(int64(input), 0)
 		return nil
-	case int64:
-		t.Time = time.Unix(input, 0)
-		return nil
 	case float64:
 		t.Time = time.Unix(int64(input), 0)
 		return nil


Test patch:
diff --git a/graphql_test.go b/graphql_test.go
index a6a318d..ba4a3eb 100644
--- a/graphql_test.go
+++ b/graphql_test.go
@@ -313,236 +313,6 @@ func TestHelloSnakeArguments(t *testing.T) {
 	})
 }
 
-func TestRootOperations_invalidSchema(t *testing.T) {
-	type args struct {
-		Schema string
-	}
-	type want struct {
-		Error string
-	}
-	testTable := map[string]struct {
-		Args args
-		Want want
-	}{
-		"Empty schema": {
-			Want: want{Error: `root operation "query" must be defined`},
-		},
-		"Query declared by schema, but type not present": {
-			Args: args{
-				Schema: `
-					schema {
-						query: Query
-					}
-				`,
-			},
-			Want: want{Error: `graphql: type "Query" not found`},
-		},
-		"Query as incorrect type": {
-			Args: args{
-				Schema: `
-					schema {
-						query: String
-					}
-				`,
-			},
-			Want: want{Error: `root operation "query" must be an OBJECT`},
-		},
-		"Query with custom name, schema omitted": {
-			Args: args{
-				Schema: `
-					type QueryType {
-						hello: String!
-					}
-				`,
-			},
-			Want: want{Error: `root operation "query" must be defined`},
-		},
-		"Mutation as incorrect type": {
-			Args: args{
-				Schema: `
-					schema {
-						query: Query
-						mutation: String
-					}
-					type Query {
-						thing: String
-					}
-				`,
-			},
-			Want: want{Error: `root operation "mutation" must be an OBJECT`},
-		},
-		"Mutation declared by schema, but type not present": {
-			Args: args{
-				Schema: `
-					schema {
-						query: Query
-						mutation: Mutation
-					}
-					type Query {
-						hello: String!
-					}
-				`,
-			},
-			Want: want{Error: `graphql: type "Mutation" not found`},
-		},
-	}
-
-	for name, tt := range testTable {
-		tt := tt
-		t.Run(name, func(t *testing.T) {
-			t.Parallel()
-
-			_, err := graphql.ParseSchema(tt.Args.Schema, nil)
-			if err == nil || err.Error() != tt.Want.Error {
-				t.Logf("got:  %v", err)
-				t.Logf("want: %s", tt.Want.Error)
-				t.Fail()
-			}
-		})
-	}
-}
-
-func TestRootOperations_validSchema(t *testing.T) {
-	type resolver struct {
-		helloSaidResolver
-		helloWorldResolver1
-		theNumberResolver
-	}
-	gqltesting.RunTests(t, []*gqltesting.Test{
-		{
-			// Query only, default name with `schema` omitted
-			Schema: graphql.MustParseSchema(`
-				type Query {
-					hello: String!
-				}
-			`, &resolver{}),
-			Query:          `{ hello }`,
-			ExpectedResult: `{"hello": "Hello world!"}`,
-		},
-		{
-			// Query only, default name with `schema` present
-			Schema: graphql.MustParseSchema(`
-				schema {
-					query: Query
-				}
-				type Query {
-					hello: String!
-				}
-			`, &resolver{}),
-			Query:          `{ hello }`,
-			ExpectedResult: `{"hello": "Hello world!"}`,
-		},
-		{
-			// Query only, custom name
-			Schema: graphql.MustParseSchema(`
-				schema {
-					query: QueryType
-				}
-				type QueryType {
-					hello: String!
-				}
-			`, &resolver{}),
-			Query:          `{ hello }`,
-			ExpectedResult: `{"hello": "Hello world!"}`,
-		},
-		{
-			// Query+Mutation, default names with `schema` omitted
-			Schema: graphql.MustParseSchema(`
-				type Query {
-					hello: String!
-				}
-				type Mutation {
-					changeTheNumber(newNumber: Int!): ChangedNumber!
-				}
-				type ChangedNumber {
-					theNumber: Int!
-				}
-			`, &resolver{}),
-			Query: `
-				mutation {
-					changeTheNumber(newNumber: 1) {
-						theNumber
-					}
-				}
-			`,
-			ExpectedResult: `{"changeTheNumber": {"theNumber": 1}}`,
-		},
-		{
-			// Query+Mutation, custom names
-			Schema: graphql.MustParseSchema(`
-				schema {
-					query: QueryType
-					mutation: MutationType
-				}
-				type QueryType {
-					hello: String!
-				}
-				type MutationType {
-					changeTheNumber(newNumber: Int!): ChangedNumber!
-				}
-				type ChangedNumber {
-					theNumber: Int!
-				}
-			`, &resolver{}),
-			Query: `
-				mutation {
-					changeTheNumber(newNumber: 1) {
-						theNumber
-					}
-				}
-			`,
-			ExpectedResult: `{"changeTheNumber": {"theNumber": 1}}`,
-		},
-		{
-			// Mutation with custom name, schema omitted
-			Schema: graphql.MustParseSchema(`
-				type Query {
-					hello: String!
-				}
-				type MutationType {
-					changeTheNumber(newNumber: Int!): ChangedNumber!
-				}
-				type ChangedNumber {
-					theNumber: Int!
-				}
-			`, &resolver{}),
-			Query: `
-				mutation {
-					changeTheNumber(newNumber: 1) {
-						theNumber
-					}
-				}
-			`,
-			ExpectedErrors: []*gqlerrors.QueryError{{Message: "no mutations are offered by the schema"}},
-		},
-		{
-			// Explicit schema without mutation field
-			Schema: graphql.MustParseSchema(`
-				schema {
-					query: Query
-				}
-				type Query {
-					hello: String!
-				}
-				type Mutation {
-					changeTheNumber(newNumber: Int!): ChangedNumber!
-				}
-				type ChangedNumber {
-					theNumber: Int!
-				}
-			`, &resolver{}),
-			Query: `
-				mutation {
-					changeTheNumber(newNumber: 1) {
-						theNumber
-					}
-				}
-			`,
-			ExpectedErrors: []*gqlerrors.QueryError{{Message: "no mutations are offered by the schema"}},
-		},
-	})
-}
-
 func TestBasic(t *testing.T) {
 	gqltesting.RunTests(t, []*gqltesting.Test{
 		{
@@ -2926,179 +2696,6 @@ func TestInput(t *testing.T) {
 	})
 }
 
-type inputArgumentsHello struct {}
-
-type inputArgumentsScalarMismatch1 struct{}
-
-type inputArgumentsScalarMismatch2 struct{}
-
-type inputArgumentsObjectMismatch1 struct{}
-
-type inputArgumentsObjectMismatch2 struct{}
-
-type inputArgumentsObjectMismatch3 struct{}
-
-type helloInput struct {
-	Name string
-}
-
-type helloInputMismatch struct {
-	World string
-}
-
-func (r *inputArgumentsHello) Hello(args struct { Input *helloInput }) string {
-	return "Hello " + args.Input.Name + "!"
-}
-
-func (r *inputArgumentsScalarMismatch1) Hello(name string) string {
-	return "Hello " + name + "!"
-}
-
-func (r *inputArgumentsScalarMismatch2) Hello(args struct { World string }) string {
-	return "Hello " + args.World + "!"
-}
-
-func (r *inputArgumentsObjectMismatch1) Hello(in helloInput) string {
-	return "Hello " + in.Name + "!"
-}
-
-func (r *inputArgumentsObjectMismatch2) Hello(args struct { Input *helloInputMismatch }) string {
-	return "Hello " + args.Input.World + "!"
-}
-
-func (r *inputArgumentsObjectMismatch3) Hello(args struct { Input *struct { Thing string } }) string {
-	return "Hello " + args.Input.Thing + "!"
-}
-
-func TestInputArguments_failSchemaParsing(t *testing.T) {
-	type args struct {
-		Resolver interface{}
-		Schema   string
-	}
-	type want struct {
-		Error string
-	}
-	testTable := map[string]struct {
-		Args args
-		Want want
-	}{
-		"Non-input type used with field arguments": {
-			Args: args{
-				Resolver: &inputArgumentsHello{},
-				Schema: `
-					schema {
-						query: Query
-					}
-					type Query {
-						hello(input: HelloInput): String!
-					}
-					type HelloInput {
-						name: String
-					}
-				`,
-			},
-			Want: want{Error: "field \"Input\": type of kind OBJECT can not be used as input\n\tused by (*graphql_test.inputArgumentsHello).Hello"},
-		},
-		"Missing Args Wrapper for scalar input": {
-			Args: args{
-				Resolver: &inputArgumentsScalarMismatch1{},
-				Schema: `
-					schema {
-						query: Query
-					}
-					type Query {
-						hello(name: String): String!
-					}
-					input HelloInput {
-						name: String
-					}
-				`,
-			},
-			Want: want{Error: "expected struct or pointer to struct, got string (hint: missing `args struct { ... }` wrapper for field arguments?)\n\tused by (*graphql_test.inputArgumentsScalarMismatch1).Hello"},
-		},
-		"Mismatching field name for scalar input": {
-			Args: args{
-				Resolver: &inputArgumentsScalarMismatch2{},
-				Schema: `
-					schema {
-						query: Query
-					}
-					type Query {
-						hello(name: String): String!
-					}
-				`,
-			},
-			Want: want{Error: "struct { World string } does not define field \"name\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsScalarMismatch2).Hello"},
-		},
-		"Missing Args Wrapper for Input type": {
-			Args: args{
-				Resolver: &inputArgumentsObjectMismatch1{},
-				Schema: `
-					schema {
-						query: Query
-					}
-					type Query {
-						hello(input: HelloInput): String!
-					}
-					input HelloInput {
-						name: String
-					}
-				`,
-			},
-			Want: want{Error: "graphql_test.helloInput does not define field \"input\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsObjectMismatch1).Hello"},
-		},
-		"Input struct missing field": {
-			Args: args{
-				Resolver: &inputArgumentsObjectMismatch2{},
-				Schema: `
-					schema {
-						query: Query
-					}
-					type Query {
-						hello(input: HelloInput): String!
-					}
-					input HelloInput {
-						name: String
-					}
-				`,
-			},
-			Want: want{Error: "field \"Input\": *graphql_test.helloInputMismatch does not define field \"name\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsObjectMismatch2).Hello"},
-		},
-		"Inline Input struct missing field": {
-			Args: args{
-				Resolver: &inputArgumentsObjectMismatch3{},
-				Schema: `
-					schema {
-						query: Query
-					}
-					type Query {
-						hello(input: HelloInput): String!
-					}
-					input HelloInput {
-						name: String
-					}
-				`,
-			},
-			Want: want{Error: "field \"Input\": *struct { Thing string } does not define field \"name\" (hint: missing `args struct { ... }` wrapper for field arguments, or missing field on input struct)\n\tused by (*graphql_test.inputArgumentsObjectMismatch3).Hello"},
-		},
-	}
-
-	for name, tt := range testTable {
-		tt := tt
-		t.Run(name, func(t *testing.T) {
-			t.Parallel()
-
-			_, err := graphql.ParseSchema(tt.Args.Schema, tt.Args.Resolver)
-			if err == nil || err.Error() != tt.Want.Error {
-				t.Log("Schema parsing error mismatch")
-				t.Logf("got: %s", err)
-				t.Logf("exp: %s", tt.Want.Error)
-				t.Fail()
-			}
-		})
-	}
-}
-
 func TestComposedFragments(t *testing.T) {
 	gqltesting.RunTests(t, []*gqltesting.Test{
 		{
@@ -3607,22 +3204,16 @@ func (r *subscriptionsInExecResolver) AppUpdated() <-chan string {
 }
 
 func TestSubscriptions_In_Exec(t *testing.T) {
-	r := &struct {
-		*helloResolver
-		*subscriptionsInExecResolver
-	}{
-		helloResolver:               &helloResolver{},
-		subscriptionsInExecResolver: &subscriptionsInExecResolver{},
-	}
 	gqltesting.RunTest(t, &gqltesting.Test{
 		Schema: graphql.MustParseSchema(`
-			type Query {
-				hello: String!
+			schema {
+				subscription: Subscription
 			}
+
 			type Subscription {
 				appUpdated : String!
 			}
-		`, r),
+	`, &subscriptionsInExecResolver{}),
 		Query: `
 			subscription {
 				appUpdated
diff --git a/internal/schema/schema_test.go b/internal/schema/schema_test.go
index 1eaae71..10fb841 100644
--- a/internal/schema/schema_test.go
+++ b/internal/schema/schema_test.go
@@ -73,7 +73,7 @@ func TestParse(t *testing.T) {
 			},
 		},
 		{
-			name: "Parses type with multi-line description string",
+			name: "Parses type with simple multi-line 'BlockString' description",
 			sdl: `
 			"""
 			Multi-line description.
@@ -95,7 +95,120 @@ func TestParse(t *testing.T) {
 			},
 		},
 		{
-			name: "Parses type with multi-line description and ignores comments",
+			name: "Parses type with empty multi-line 'BlockString' description",
+			sdl: `
+			"""
+			"""
+			type Type {
+				field: String
+			}`,
+			useStringDescriptions: true,
+			validateSchema: func(s *schema.Schema) error {
+				const typeName = "Type"
+				typ, ok := s.Types[typeName].(*schema.Object)
+				if !ok {
+					return fmt.Errorf("type %q not found", typeName)
+				}
+				if want, have := "", typ.Description(); want != have {
+					return fmt.Errorf("invalid description: want %q, have %q", want, have)
+				}
+				return nil
+			},
+		},
+		{
+			name: "Parses type with multi-line 'BlockString' description",
+			sdl: `
+			"""
+			First line of the description.
+
+			Second line of the description.
+
+				query {
+					code {
+						example
+					}
+				}
+
+			Notes:
+
+			 * First note
+			 * Second note
+			"""
+			type Type {
+				field: String
+			}`,
+			useStringDescriptions: true,
+			validateSchema: func(s *schema.Schema) error {
+				const typeName = "Type"
+				typ, ok := s.Types[typeName].(*schema.Object)
+				if !ok {
+					return fmt.Errorf("type %q not found", typeName)
+				}
+				want := "First line of the description.\n\nSecond line of the description.\n\n\tquery {\n\t\tcode {\n\t\t\texample\n\t\t}\n\t}\n\nNotes:\n\n * First note\n * Second note"
+				if have := typ.Description(); want != have {
+					return fmt.Errorf("invalid description: want %q, have %q", want, have)
+				}
+				return nil
+			},
+		},
+		{
+			name: "Parses type with un-indented multi-line 'BlockString' description",
+			sdl: `
+			"""
+First line of the description.
+
+Second line of the description.
+			"""
+			type Type {
+				field: String
+			}`,
+			useStringDescriptions: true,
+			validateSchema: func(s *schema.Schema) error {
+				const typeName = "Type"
+				typ, ok := s.Types[typeName].(*schema.Object)
+				if !ok {
+					return fmt.Errorf("type %q not found", typeName)
+				}
+				want := "First line of the description.\n\nSecond line of the description."
+				if have := typ.Description(); want != have {
+					return fmt.Errorf("invalid description: want %q, have %q", want, have)
+				}
+				return nil
+			},
+		},
+		{
+			name: "Parses type with space-indented multi-line 'BlockString' description",
+			sdl: `
+            """
+            First line of the description.
+
+            Second line of the description.
+
+                query {
+                    code {
+                        example
+                    }
+                }
+            """
+            type Type {
+                field: String
+            }`,
+			useStringDescriptions: true,
+			validateSchema: func(s *schema.Schema) error {
+				const typeName = "Type"
+				typ, ok := s.Types[typeName].(*schema.Object)
+				if !ok {
+					return fmt.Errorf("type %q not found", typeName)
+				}
+				want := "First line of the description.\n\nSecond line of the description.\n\n    query {\n        code {\n            example\n        }\n    }"
+				if have := typ.Description(); want != have {
+					return fmt.Errorf("invalid description: want %q, have %q", want, have)
+				}
+				return nil
+			},
+		},
+		{
+			name: "Parses type with multi-line 'BlockString' description and ignores comments",
 			sdl: `
 			"""
 			Multi-line description with ignored comments.
@@ -165,46 +278,6 @@ func TestParse(t *testing.T) {
 				return nil
 			},
 		},
-		{
-			name: "Default Root schema",
-			sdl: `
-			type Query {
-				hello: String!
-			}
-			type Mutation {
-				concat(a: String!, b: String!): String!
-			}
-			`,
-			validateSchema: func(s *schema.Schema) error {
-				typq, ok := s.Types["Query"].(*schema.Object)
-				if !ok {
-					return fmt.Errorf("type %q not found", "Query")
-				}
-				helloField := typq.Fields.Get("hello")
-				if helloField == nil {
-					return fmt.Errorf("field %q not found", "hello")
-				}
-				if helloField.Type.String() != "String!" {
-					return fmt.Errorf("field %q has an invalid type: %q", "hello", helloField.Type.String())
-				}
-
-				typm, ok := s.Types["Mutation"].(*schema.Object)
-				if !ok {
-					return fmt.Errorf("type %q not found", "Mutation")
-				}
-				concatField := typm.Fields.Get("concat")
-				if concatField == nil {
-					return fmt.Errorf("field %q not found", "concat")
-				}
-				if concatField.Type.String() != "String!" {
-					return fmt.Errorf("field %q has an invalid type: %q", "concat", concatField.Type.String())
-				}
-				if len(concatField.Args) != 2 || concatField.Args[0] == nil || concatField.Args[1] == nil || concatField.Args[0].Type.String() != "String!" || concatField.Args[1].Type.String() != "String!" {
-					return fmt.Errorf("field %q has an invalid args: %+v", "concat", concatField.Args)
-				}
-				return nil
-			},
-		},
 		{
 			name: "Extend type",
 			sdl: `
diff --git a/subscription_test.go b/subscription_test.go
index 5b8c56b..7a7de6e 100644
--- a/subscription_test.go
+++ b/subscription_test.go
@@ -276,146 +276,6 @@ func TestSchemaSubscribe(t *testing.T) {
 	})
 }
 
-func TestRootOperations_invalidSubscriptionSchema(t *testing.T) {
-	type args struct {
-		Schema string
-	}
-	type want struct {
-		Error string
-	}
-	testTable := map[string]struct {
-		Args args
-		Want want
-	}{
-		"Subscription as incorrect type": {
-			Args: args{
-				Schema: `
-					schema {
-						query: Query
-						subscription: String
-					}
-					type Query {
-						thing: String
-					}
-				`,
-			},
-			Want: want{Error: `root operation "subscription" must be an OBJECT`},
-		},
-		"Subscription declared by schema, but type not present": {
-			Args: args{
-				Schema: `
-					schema {
-						query: Query
-						subscription: Subscription
-					}
-					type Query {
-						hello: String!
-					}
-				`,
-			},
-			Want: want{Error: `graphql: type "Subscription" not found`},
-		},
-	}
-
-	for name, tt := range testTable {
-		tt := tt
-		t.Run(name, func(t *testing.T) {
-			t.Parallel()
-
-			_, err := graphql.ParseSchema(tt.Args.Schema, nil)
-			if err == nil || err.Error() != tt.Want.Error {
-				t.Logf("got:  %v", err)
-				t.Logf("want: %s", tt.Want.Error)
-				t.Fail()
-			}
-		})
-	}
-}
-
-func TestRootOperations_validSubscriptionSchema(t *testing.T) {
-	gqltesting.RunSubscribes(t, []*gqltesting.TestSubscription{
-		{
-			Name: "Default name, schema omitted",
-			Schema: graphql.MustParseSchema(`
-				type Query {
-					hello: String!
-				}
-				type Subscription {
-					helloSaid: HelloSaidEvent!
-				}
-				type HelloSaidEvent {
-					msg: String!
-				}
-			`, &rootResolver{helloSaidResolver: &helloSaidResolver{upstream: closedUpstream(&helloSaidEventResolver{msg: "Hello world!"})}}),
-			Query: `subscription { helloSaid { msg } }`,
-			ExpectedResults: []gqltesting.TestResponse{
-				{
-					Data: json.RawMessage(`{"helloSaid": {"msg": "Hello world!"}}`),
-				},
-			},
-		},
-		{
-			Name: "Custom name, schema omitted",
-			Schema: graphql.MustParseSchema(`
-				type Query {
-					hello: String!
-				}
-				type SubscriptionType {
-					helloSaid: HelloSaidEvent!
-				}
-				type HelloSaidEvent {
-					msg: String!
-				}
-			`, &rootResolver{}),
-			Query:       `subscription { helloSaid { msg } }`,
-			ExpectedErr: errors.New("no subscriptions are offered by the schema"),
-		},
-		{
-			Name: "Custom name, schema required",
-			Schema: graphql.MustParseSchema(`
-					schema {
-						query: Query
-						subscription: SubscriptionType
-					}
-					type Query {
-						hello: String!
-					}
-					type SubscriptionType {
-						helloSaid: HelloSaidEvent!
-					}
-					type HelloSaidEvent {
-						msg: String!
-					}
-			`, &rootResolver{helloSaidResolver: &helloSaidResolver{upstream: closedUpstream(&helloSaidEventResolver{msg: "Hello world!"})}}),
-			Query: `subscription { helloSaid { msg } }`,
-			ExpectedResults: []gqltesting.TestResponse{
-				{
-					Data: json.RawMessage(`{"helloSaid": {"msg": "Hello world!"}}`),
-				},
-			},
-		},
-		{
-			Name: "Explicit schema without subscription field",
-			Schema: graphql.MustParseSchema(`
-					schema {
-						query: Query
-					}
-					type Query {
-						hello: String!
-					}
-					type Subscription {
-						helloSaid: HelloSaidEvent!
-					}
-					type HelloSaidEvent {
-						msg: String!
-					}
-			`, &rootResolver{helloSaidResolver: &helloSaidResolver{upstream: closedUpstream(&helloSaidEventResolver{msg: "Hello world!"})}}),
-			Query:       `subscription { helloSaid { msg } }`,
-			ExpectedErr: errors.New("no subscriptions are offered by the schema"),
-		},
-	})
-}
-
 const schema = `
 	schema {
 		subscription: Subscription,