Repository: jensneuse/graphql-go-tools
Pull request #102 (2019-11-14 19:57:52)

ObjectType extension and extension removal
This PR will implement the ast_normalization functionality to extend object types by ObjectTypeExtensions and the extension removal function which is responsible for removing merged extensions.
Note that removeMergedTypeExtensions' test will be expanded as new types of extension merging are implemented.


Patch:
diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go
index 0345517b..4d4b735b 100644
--- a/pkg/ast/ast.go
+++ b/pkg/ast/ast.go
@@ -456,6 +456,35 @@ func (d *Document) FieldDefinitionTypeNode(ref int) Node {
 	return d.Index.Nodes[xxhash.Sum64(typeName)]
 }
 
+func (d *Document) ExtendObjectTypeDefinitionByObjectTypeExtension(objectTypeDefinitionRef, objectTypeExtensionRef int) {
+	if d.ObjectTypeExtensionHasFieldDefinitions(objectTypeExtensionRef) {
+		d.ObjectTypeDefinitions[objectTypeDefinitionRef].FieldsDefinition.Refs = append(d.ObjectTypeDefinitions[objectTypeDefinitionRef].FieldsDefinition.Refs, d.ObjectTypeExtensions[objectTypeExtensionRef].FieldsDefinition.Refs...)
+		d.ObjectTypeDefinitions[objectTypeDefinitionRef].HasFieldDefinitions = true
+	}
+
+	if d.ObjectTypeExtensionHasDirectives(objectTypeExtensionRef) {
+		d.ObjectTypeDefinitions[objectTypeDefinitionRef].Directives.Refs = append(d.ObjectTypeDefinitions[objectTypeDefinitionRef].Directives.Refs, d.ObjectTypeExtensions[objectTypeExtensionRef].Directives.Refs...)
+		d.ObjectTypeDefinitions[objectTypeDefinitionRef].HasDirectives = true
+	}
+
+	d.Index.MergedTypeExtensions = append(d.Index.MergedTypeExtensions, Node{Ref: objectTypeExtensionRef, Kind: NodeKindObjectTypeExtension})
+}
+
+func (d *Document) RemoveMergedTypeExtensions() {
+	for _, node := range d.Index.MergedTypeExtensions {
+		d.RemoveRootNode(node)
+	}
+}
+
+func (d *Document) RemoveRootNode(node Node) {
+	for i := range d.RootNodes {
+		if d.RootNodes[i] == node {
+			d.RootNodes = append(d.RootNodes[:i], d.RootNodes[i+1:]...)
+			return
+		}
+	}
+}
+
 func (d *Document) NodeFieldDefinitions(node Node) []int {
 	switch node.Kind {
 	case NodeKindObjectTypeDefinition:
@@ -1665,6 +1694,14 @@ func (d *Document) ObjectTypeExtensionNameString(ref int) string {
 	return unsafebytes.BytesToString(d.Input.ByteSlice(d.ObjectTypeExtensions[ref].Name))
 }
 
+func (d *Document) ObjectTypeExtensionHasFieldDefinitions(ref int) bool {
+	return d.ObjectTypeExtensions[ref].HasFieldDefinitions
+}
+
+func (d *Document) ObjectTypeExtensionHasDirectives(ref int) bool {
+	return d.ObjectTypeExtensions[ref].HasDirectives
+}
+
 type InputValueDefinition struct {
 	Description   Description        // optional, e.g. "input Foo is..."
 	Name          ByteSliceReference // e.g. Foo
diff --git a/pkg/ast/index.go b/pkg/ast/index.go
index e8ca9044..e51a87d9 100644
--- a/pkg/ast/index.go
+++ b/pkg/ast/index.go
@@ -7,6 +7,7 @@ type Index struct {
 	SubscriptionTypeName    ByteSlice
 	Nodes                   map[uint64]Node
 	ReplacedFragmentSpreads []int
+	MergedTypeExtensions    []Node
 }
 
 func (i *Index) Reset() {
@@ -14,6 +15,7 @@ func (i *Index) Reset() {
 	i.MutationTypeName = i.MutationTypeName[:0]
 	i.SubscriptionTypeName = i.SubscriptionTypeName[:0]
 	i.ReplacedFragmentSpreads = i.ReplacedFragmentSpreads[:0]
+	i.MergedTypeExtensions = i.MergedTypeExtensions[:0]
 	for j := range i.Nodes {
 		delete(i.Nodes, j)
 	}
diff --git a/pkg/astnormalization/object_type_extending.go b/pkg/astnormalization/object_type_extending.go
new file mode 100644
index 00000000..fd293628
--- /dev/null
+++ b/pkg/astnormalization/object_type_extending.go
@@ -0,0 +1,38 @@
+package astnormalization
+
+import (
+	"github.com/cespare/xxhash"
+	"github.com/jensneuse/graphql-go-tools/pkg/ast"
+	"github.com/jensneuse/graphql-go-tools/pkg/astvisitor"
+)
+
+func extendObjectTypeDefinition(walker *astvisitor.Walker) {
+	visitor := extendObjectTypeDefinitionVisitor{
+		Walker: walker,
+	}
+	walker.RegisterEnterDocumentVisitor(&visitor)
+	walker.RegisterEnterObjectTypeExtensionVisitor(&visitor)
+}
+
+type extendObjectTypeDefinitionVisitor struct {
+	*astvisitor.Walker
+	operation *ast.Document
+}
+
+func (e *extendObjectTypeDefinitionVisitor) EnterDocument(operation, definition *ast.Document) {
+	e.operation = operation
+}
+
+func (e *extendObjectTypeDefinitionVisitor) EnterObjectTypeExtension(ref int) {
+
+	baseNode, exists := e.operation.Index.Nodes[xxhash.Sum64(e.operation.ObjectTypeExtensionNameBytes(ref))]
+	if !exists {
+		return
+	}
+
+	if baseNode.Kind != ast.NodeKindObjectTypeDefinition {
+		return
+	}
+
+	e.operation.ExtendObjectTypeDefinitionByObjectTypeExtension(baseNode.Ref, ref)
+}
diff --git a/pkg/astnormalization/remove_type_extensions.go b/pkg/astnormalization/remove_type_extensions.go
new file mode 100644
index 00000000..b4202026
--- /dev/null
+++ b/pkg/astnormalization/remove_type_extensions.go
@@ -0,0 +1,21 @@
+package astnormalization
+
+import (
+	"github.com/jensneuse/graphql-go-tools/pkg/ast"
+	"github.com/jensneuse/graphql-go-tools/pkg/astvisitor"
+)
+
+func removeMergedTypeExtensions(walker *astvisitor.Walker) {
+	visitor := removeMergedTypeExtensionsVisitor{
+		Walker: walker,
+	}
+	walker.RegisterLeaveDocumentVisitor(&visitor)
+}
+
+type removeMergedTypeExtensionsVisitor struct {
+	*astvisitor.Walker
+}
+
+func (r *removeMergedTypeExtensionsVisitor) LeaveDocument(operation, definition *ast.Document) {
+	operation.RemoveMergedTypeExtensions()
+}


Test patch:
diff --git a/pkg/astnormalization/astnormalization_test.go b/pkg/astnormalization/astnormalization_test.go
index 71a46d66..76773589 100644
--- a/pkg/astnormalization/astnormalization_test.go
+++ b/pkg/astnormalization/astnormalization_test.go
@@ -143,6 +143,7 @@ var run = func(normalizeFunc registerNormalizeFunc, definition, operation, expec
 	expectedOutputDocument := unsafeparser.ParseGraphqlDocumentString(expectedOutput)
 	report := operationreport.Report{}
 	walker := astvisitor.NewWalker(48)
+
 	normalizeFunc(&walker)
 
 	walker.Walk(&operationDocument, &definitionDocument, &report)
@@ -159,6 +160,16 @@ var run = func(normalizeFunc registerNormalizeFunc, definition, operation, expec
 	}
 }
 
+func runMany(definition, operation, expectedOutput string, normalizeFuncs ...registerNormalizeFunc) {
+	var runManyNormalizers = func(walker *astvisitor.Walker) {
+		for _, normalizeFunc := range normalizeFuncs {
+			normalizeFunc(walker)
+		}
+	}
+
+	run(runManyNormalizers, definition, operation, expectedOutput)
+}
+
 const testOperation = `	
 subscription sub {
 	... multipleSubscriptions
diff --git a/pkg/astnormalization/object_type_extending_test.go b/pkg/astnormalization/object_type_extending_test.go
new file mode 100644
index 00000000..037c35fe
--- /dev/null
+++ b/pkg/astnormalization/object_type_extending_test.go
@@ -0,0 +1,92 @@
+package astnormalization
+
+import "testing"
+
+func TestExtendObjectType(t *testing.T) {
+	t.Run("extend simple object type by field", func(t *testing.T) {
+		run(extendObjectTypeDefinition, testDefinition, `
+					type Dog {
+						name: String
+					}
+					extend type Dog {
+						favoriteToy: String
+					}
+					 `, `
+					type Dog {
+						name: String
+						favoriteToy: String
+					}
+					extend type Dog {
+						favoriteToy: String
+					}
+					`)
+	})
+	t.Run("extend simple object type by directive", func(t *testing.T) {
+		run(extendObjectTypeDefinition, testDefinition, `
+					type Cat {
+						name: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs")
+					 `, `
+					type Cat @deprecated(reason: "not as cool as dogs") {
+						name: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs")
+					`)
+	})
+	t.Run("extend object type by multiple field", func(t *testing.T) {
+		run(extendObjectTypeDefinition, testDefinition, `
+					type Dog {
+						name: String
+					}
+					extend type Dog {
+						favoriteToy: String
+						breed: String
+					}
+					 `, `
+					type Dog {
+						name: String
+						favoriteToy: String
+						breed: String
+					}
+					extend type Dog {
+						favoriteToy: String
+						breed: String
+					}
+					`)
+	})
+	t.Run("extend object type by multiple directives", func(t *testing.T) {
+		run(extendObjectTypeDefinition, testDefinition, `
+					type Cat {
+						name: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs") @skip(if: false)
+					 `, `
+					type Cat @deprecated(reason: "not as cool as dogs") @skip(if: false) {
+						name: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs") @skip(if: false)
+					`)
+	})
+	t.Run("extend object type by complex extends", func(t *testing.T) {
+		run(extendObjectTypeDefinition, testDefinition, `
+					type Cat {
+						name: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs") @skip(if: false) {
+						age: Int
+						breed: String
+					}
+					 `, `
+					type Cat @deprecated(reason: "not as cool as dogs") @skip(if: false) {
+						name: String
+						age: Int
+						breed: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs") @skip(if: false) {
+						age: Int
+						breed: String
+					}
+					`)
+	})
+}
diff --git a/pkg/astnormalization/remove_type_extensions_test.go b/pkg/astnormalization/remove_type_extensions_test.go
new file mode 100644
index 00000000..5fca0ac4
--- /dev/null
+++ b/pkg/astnormalization/remove_type_extensions_test.go
@@ -0,0 +1,55 @@
+package astnormalization
+
+import "testing"
+
+func TestRemoveTypeExtensions(t *testing.T) {
+	t.Run("remove single type extension of fieldDefinition", func(t *testing.T) {
+		runMany(testDefinition, `
+					type Dog {
+						name: String
+					}
+					extend type Dog {
+						favoriteToy: String
+					}
+					 `, `
+					type Dog {
+						name: String
+						favoriteToy: String
+					}
+					`,
+			extendObjectTypeDefinition,
+			removeMergedTypeExtensions)
+	})
+	t.Run("remove single type extension of directive", func(t *testing.T) {
+		runMany(testDefinition, `
+					type Cat {
+						name: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs")
+					 `, `
+					type Cat @deprecated(reason: "not as cool as dogs") {
+						name: String
+					}
+					`,
+			extendObjectTypeDefinition,
+			removeMergedTypeExtensions)
+	})
+	t.Run("remove multiple type extensions at once", func(t *testing.T) {
+		runMany(testDefinition, `
+					type Cat {
+						name: String
+					}
+					extend type Cat @deprecated(reason: "not as cool as dogs")
+					extend type Cat {
+						age: Int
+					}
+					 `, `
+					type Cat @deprecated(reason: "not as cool as dogs") {
+						name: String
+						age: Int
+					}
+					`,
+			extendObjectTypeDefinition,
+			removeMergedTypeExtensions)
+	})
+}