Skip to content

Commit

Permalink
Merge pull request #4 from mjarkk/support-graphql-interfaces
Browse files Browse the repository at this point in the history
Support graphql interfaces
  • Loading branch information
mjarkk committed Oct 9, 2021
2 parents 1e4fd80 + e77cdda commit 7527d10
Show file tree
Hide file tree
Showing 14 changed files with 697 additions and 218 deletions.
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (QueryRoot) ResolvePosts() []Post {
type MethodRoot struct{}

func main() {
s := NewSchema()
s := graphql.NewSchema()

err := s.Parse(QueryRoot{}, MethodRoot{}, nil)
if err != nil {
Expand Down Expand Up @@ -217,9 +217,8 @@ const (
Grapefruit
)


func main() {
s := NewSchema()
s := graphql.NewSchema()

// The map key is the enum it's key in graphql
// The map value is the go value the enum key is mapped to or the other way around
Expand All @@ -234,6 +233,41 @@ func main() {
}
```

### Interfaces

Graphql interfaces can be created using go interfaces

This library needs to anylize all types before you can make a query and as we cannot query all types that implmenet a interface you'll need to help the library with this by calling `Implements` for every implementation.
If `Implements` is not called for a type the response value for that type when inside a interface will always be `null`

```go
type QuerySchema struct {
Bar BarWImpl
Baz BazWImpl
BarOrBaz InterfaceType
}

type InterfaceType interface {
// Interface fields
ResolveFoo() string
ResolveBar() string
}

type BarWImpl struct{}

// Implements hints this library to register BarWImpl
// THIS MUST BE CALLED FOR EVERY TYPE THAT IMPLMENTS InterfaceType
var _ = graphql.Implements((*InterfaceType)(nil), BarWImpl{})

func (BarWImpl) ResolveFoo() string { return "this is bar" }
func (BarWImpl) ResolveBar() string { return "This is bar" }

type BazWImpl struct{}
var _ = graphql.Implements((*InterfaceType)(nil), BazWImpl{})
func (BazWImpl) ResolveFoo() string { return "this is baz" }
func (BazWImpl) ResolveBar() string { return "This is baz" }
```

### Directives

These directives are added by default:
Expand All @@ -245,7 +279,7 @@ To add custom directives:

```go
func main() {
s := NewSchema()
s := graphql.NewSchema()

// Also the .RegisterEnum(..) method must be called before .Parse(..)
s.RegisterDirective(Directive{
Expand Down
8 changes: 4 additions & 4 deletions bytecode/bytecode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"sync"
"testing"

. "github.com/stretchr/testify/assert"
a "github.com/stretchr/testify/assert"
)

func parseQuery(query string) ([]byte, []error) {
Expand All @@ -26,9 +26,9 @@ func parseQuery(query string) ([]byte, []error) {
func parseQueryAndExpectErr(t *testing.T, query, expectedErr string) {
_, errs := parseQuery(query)
if len(errs) == 0 {
Fail(t, "exected query to fail with error: "+expectedErr, query)
a.Fail(t, "exected query to fail with error: "+expectedErr, query)
}
Equal(t, errs[0].Error(), expectedErr)
a.Equal(t, errs[0].Error(), expectedErr)
}

func newParseQueryAndExpectResult(t *testing.T, query string, expectedResult []byte, debug ...bool) {
Expand All @@ -48,7 +48,7 @@ func newParseQueryAndExpectResult(t *testing.T, query string, expectedResult []b
fmt.Println(expectedResultHex)
}

Equal(t, expectedResultHex, resHex, query)
a.Equal(t, expectedResultHex, resHex, query)
}

func TestParseSimpleQuery(t *testing.T) {
Expand Down
42 changes: 21 additions & 21 deletions enums_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,59 @@ package graphql
import (
"testing"

. "github.com/stretchr/testify/assert"
a "github.com/stretchr/testify/assert"
)

func TestRegisterEnum(t *testing.T) {
type TestEnumString string
res, err := registerEnumCheck(map[string]TestEnumString{
"A": "B",
})
NoError(t, err)
NotNil(t, res)
a.NoError(t, err)
a.NotNil(t, res)

type TestEnumUint uint
res, err = registerEnumCheck(map[string]TestEnumUint{
"A": 1,
})
NoError(t, err)
NotNil(t, res)
a.NoError(t, err)
a.NotNil(t, res)

type TestEnumInt uint
res, err = registerEnumCheck(map[string]TestEnumInt{
"A": 1,
})
NoError(t, err)
NotNil(t, res)
a.NoError(t, err)
a.NotNil(t, res)
}

func TestEmptyEnumShouldNotBeRegistered(t *testing.T) {
type TestEnum string
res, err := registerEnumCheck(map[string]TestEnum{})
NoError(t, err)
Nil(t, res)
a.NoError(t, err)
a.Nil(t, res)
}

func TestRegisterEnumFails(t *testing.T) {
type TestEnum string

_, err := registerEnumCheck(0)
Error(t, err, "Cannot generate an enum of non map types")
a.Error(t, err, "Cannot generate an enum of non map types")

_, err = registerEnumCheck(nil)
Error(t, err, "Cannot generate an enum of non map types 2")
a.Error(t, err, "Cannot generate an enum of non map types 2")

_, err = registerEnumCheck(map[int]TestEnum{1: "a"})
Error(t, err, "Enum must have a string key type")
a.Error(t, err, "Enum must have a string key type")

_, err = registerEnumCheck(map[string]struct{}{"a": {}})
Error(t, err, "Enum value cannot be complex")
a.Error(t, err, "Enum value cannot be complex")

_, err = registerEnumCheck(map[string]string{"foo": "bar"})
Error(t, err, "Enum value must be a custom type")
a.Error(t, err, "Enum value must be a custom type")

_, err = registerEnumCheck(map[string]TestEnum{"": ""})
Error(t, err, "Enum keys cannot be empty")
a.Error(t, err, "Enum keys cannot be empty")

// Maybe fix this??
// _, err = registerEnumCheck(map[string]TestEnum{
Expand All @@ -65,13 +65,13 @@ func TestRegisterEnumFails(t *testing.T) {
// Error(t, err, "Enum cannot have duplicated values")

_, err = registerEnumCheck(map[string]TestEnum{"1": ""})
Error(t, err, "Enum cannot have an invalid graphql name, where first letter is number")
a.Error(t, err, "Enum cannot have an invalid graphql name, where first letter is number")

_, err = registerEnumCheck(map[string]TestEnum{"_": ""})
Error(t, err, "Enum cannot have an invalid graphql name, where first letter is underscore")
a.Error(t, err, "Enum cannot have an invalid graphql name, where first letter is underscore")

_, err = registerEnumCheck(map[string]TestEnum{"A!!!!": ""})
Error(t, err, "Enum cannot have an invalid graphql name, where remainder of name is invalid")
a.Error(t, err, "Enum cannot have an invalid graphql name, where remainder of name is invalid")
}

type TestEnum2 uint8
Expand All @@ -96,12 +96,12 @@ func TestEnum(t *testing.T) {
"BAR": TestEnum2Bar,
"BAZ": TestEnum2Baz,
})
True(t, added)
NoError(t, err)
a.True(t, added)
a.NoError(t, err)

res, errs := bytecodeParse(t, s, `{bar(e: BAZ)}`, TestEnumFunctionInput{}, M{}, ResolveOptions{NoMeta: true})
for _, err := range errs {
panic(err)
}
Equal(t, `{"bar":"BAZ"}`, res)
a.Equal(t, `{"bar":"BAZ"}`, res)
}
2 changes: 1 addition & 1 deletion grahql_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ type qlType struct {
Interfaces []qlType `json:"interfaces"`

// INTERFACE and UNION only
PossibleTypes []qlType `json:"possibleTypes"`
PossibleTypes func() []qlType `json:"possibleTypes"`

// ENUM only
EnumValues func(isDeprecatedArgs) []qlEnumValue `json:"-"`
Expand Down
18 changes: 9 additions & 9 deletions implement_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import (
"strings"
"testing"

. "github.com/stretchr/testify/assert"
a "github.com/stretchr/testify/assert"
)

func TestHandleRequestRequestInURL(t *testing.T) {
s := NewSchema()
err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil)
NoError(t, err)
a.NoError(t, err)

res, errs := s.HandleRequest(
"GET",
Expand All @@ -31,13 +31,13 @@ func TestHandleRequestRequestInURL(t *testing.T) {
for _, err := range errs {
panic(err)
}
Equal(t, `{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}}`, string(res))
a.Equal(t, `{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}}`, string(res))
}

func TestHandleRequestRequestJsonBody(t *testing.T) {
s := NewSchema()
err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil)
NoError(t, err)
a.NoError(t, err)

query := `
query Foo {
Expand Down Expand Up @@ -71,13 +71,13 @@ func TestHandleRequestRequestJsonBody(t *testing.T) {
for _, err := range errs {
panic(err)
}
Equal(t, `{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}}`, string(res))
a.Equal(t, `{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}}`, string(res))
}

func TestHandleRequestRequestForm(t *testing.T) {
s := NewSchema()
err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil)
NoError(t, err)
a.NoError(t, err)

query := `
query Foo {
Expand Down Expand Up @@ -115,13 +115,13 @@ func TestHandleRequestRequestForm(t *testing.T) {
for _, err := range errs {
panic(err)
}
Equal(t, `{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}}`, string(res))
a.Equal(t, `{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}}`, string(res))
}

func TestHandleRequestRequestBatch(t *testing.T) {
s := NewSchema()
err := s.Parse(TestResolveSchemaRequestWithFieldsData{A: TestResolveSchemaRequestWithFieldsDataInnerStruct{Bar: "baz"}}, M{}, nil)
NoError(t, err)
a.NoError(t, err)

query := `
query Foo {
Expand Down Expand Up @@ -162,5 +162,5 @@ func TestHandleRequestRequestBatch(t *testing.T) {
for _, err := range errs {
panic(err)
}
Equal(t, `[{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}},{"data":{"a":{"foo":null}},"errors":[],"extensions":{}}]`, string(res))
a.Equal(t, `[{"data":{"a":{"bar":"baz"}},"errors":[],"extensions":{}},{"data":{"a":{"foo":null}},"errors":[],"extensions":{}}]`, string(res))
}
62 changes: 60 additions & 2 deletions inject_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ func (s *Schema) getDirectives() []qlDirective {

func (s *Schema) getAllQLTypes() []qlType {
if s.graphqlTypesList == nil {
s.graphqlTypesList = make([]qlType, len(s.types)+len(s.inTypes)+len(s.definedEnums)+len(scalars))
// Only generate s.graphqlTypesList once as the content won't change on runtime

s.graphqlTypesList = make(
[]qlType,
len(s.types)+len(s.inTypes)+len(s.definedEnums)+len(scalars)+len(s.interfaces),
)

idx := 0
for _, type_ := range s.types {
Expand All @@ -124,6 +129,11 @@ func (s *Schema) getAllQLTypes() []qlType {
s.graphqlTypesList[idx] = scalar
idx++
}
for _, interface_ := range s.interfaces {
obj, _ := s.objToQLType(interface_)
s.graphqlTypesList[idx] = *obj
idx++
}

sort.Slice(s.graphqlTypesList, func(a int, b int) bool { return *s.graphqlTypesList[a].Name < *s.graphqlTypesList[b].Name })
}
Expand Down Expand Up @@ -267,6 +277,14 @@ func (s *Schema) objToQLType(item *obj) (res *qlType, isNonNull bool) {
return s.objToQLType(s.types[item.typeName])
case valueTypeObj:
isNonNull = true
interfaces := []qlType{}
if len(item.implementations) != 0 {
for _, implementation := range item.implementations {
interface_, _ := s.objToQLType(implementation)
interfaces = append(interfaces, *interface_)
}
}

res = &qlType{
Kind: typeKindObject,
Name: &item.typeName,
Expand All @@ -290,7 +308,7 @@ func (s *Schema) objToQLType(item *obj) (res *qlType, isNonNull bool) {
s.graphqlObjFields[item.typeName] = res
return res
},
Interfaces: []qlType{},
Interfaces: interfaces,
}
return
case valueTypeEnum:
Expand All @@ -307,6 +325,46 @@ func (s *Schema) objToQLType(item *obj) (res *qlType, isNonNull bool) {
isNonNull = false
}
return
case valueTypeInterfaceRef:
return s.objToQLType(s.interfaces[item.typeName])
case valueTypeInterface:
// A interface should be non null BUT as a interface in go can be nil we set it to false
isNonNull = false

res = &qlType{
Kind: typeKindInterface,
Name: &item.typeName,
Description: h.PtrToEmptyStr,
Interfaces: []qlType{},
PossibleTypes: func() []qlType {
possibleTypes := make([]qlType, len(item.implementations))
for idx, implementation := range item.implementations {
item, _ := s.objToQLType(implementation)
possibleTypes[idx] = *item
}
return possibleTypes
},
Fields: func(args isDeprecatedArgs) []qlField {
fields, ok := s.graphqlObjFields[item.typeName]
if ok {
return fields
}

res := []qlField{}
for _, innerItem := range item.objContents {
res = append(res, qlField{
Name: string(innerItem.qlFieldName),
Args: s.getObjectArgs(innerItem),
Type: *wrapQLTypeInNonNull(s.objToQLType(innerItem)),
})
}
sort.Slice(res, func(a int, b int) bool { return res[a].Name < res[b].Name })

s.graphqlObjFields[item.typeName] = res
return res
},
}
return
default:
return resolveObjToScalar(item), true
}
Expand Down
Loading

0 comments on commit 7527d10

Please sign in to comment.