Skip to content
This repository has been archived by the owner on Apr 1, 2024. It is now read-only.

use code-hex gqlparser #16

Merged
merged 3 commits into from
Apr 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ go 1.16

require (
github.com/99designs/gqlgen v0.13.0
github.com/Code-Hex/gqlparser/v2 v2.1.1-0.20210404043438-758ac252308a
github.com/agnivade/levenshtein v1.1.0 // indirect
github.com/google/go-cmp v0.5.5
github.com/machinebox/graphql v0.2.2
github.com/matryer/is v1.4.0 // indirect
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
github.com/99designs/gqlgen v0.13.0 h1:haLTcUp3Vwp80xMVEg5KRNwzfUrgFdRmtBY8fuB8scA=
github.com/99designs/gqlgen v0.13.0/go.mod h1:NV130r6f4tpRWuAI+zsrSdooO/eWUv+Gyyoi3rEfXIk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Code-Hex/gqlparser/v2 v2.1.1-0.20210404043438-758ac252308a h1:IzMV22Hitzxqb71um5i64jEJTyeUVwemK8JmcQdgpA4=
github.com/Code-Hex/gqlparser/v2 v2.1.1-0.20210404043438-758ac252308a/go.mod h1:0sYgRh/Er/ZhHrcKw516TOX4K0ul94vKcBSXAPhLWDc=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
Expand All @@ -13,8 +16,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM=
github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
Expand Down
278 changes: 131 additions & 147 deletions internal/gqlgen/gqlgen.go

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions internal/graphql/context_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package graphql

import (
"context"
"errors"

"github.com/Code-Hex/gqlparser/v2/ast"
)

type OperationContext struct {
RawQuery string
Variables map[string]interface{}
OperationName string
Doc *ast.QueryDocument

Operation *ast.OperationDefinition
}

func (c *OperationContext) Validate(ctx context.Context) error {
if c.Doc == nil {
return errors.New("field 'Doc'is required")
}
if c.RawQuery == "" {
return errors.New("field 'RawQuery' is required")
}
if c.Variables == nil {
c.Variables = make(map[string]interface{})
}
return nil
}

type operationCtx struct{}

func GetOperationContext(ctx context.Context) *OperationContext {
if val, ok := ctx.Value(operationCtx{}).(*OperationContext); ok && val != nil {
return val
}
panic("missing operation context")
}

func WithOperationContext(ctx context.Context, rc *OperationContext) context.Context {
return context.WithValue(ctx, operationCtx{}, rc)
}
20 changes: 20 additions & 0 deletions internal/graphql/context_operation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package graphql

import (
"context"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestGetOperationContext(t *testing.T) {
rc := &OperationContext{}

ctx := WithOperationContext(context.Background(), rc)

got := GetOperationContext(ctx)

if diff := cmp.Diff(rc, got); diff != "" {
t.Errorf("(-want, +got)\n%s", diff)
}
}
134 changes: 134 additions & 0 deletions internal/graphql/executable_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package graphql

import (
"fmt"

"github.com/Code-Hex/gqlparser/v2/ast"
)

// CollectFields returns the set of fields from an ast.SelectionSet where all collected fields satisfy at least one of the GraphQL types
// passed through satisfies. Providing an empty or nil slice for satisfies will return collect all fields regardless of fragment
// type conditions.
func CollectFields(reqCtx *OperationContext, selSet ast.SelectionSet, satisfies []string) []CollectedField {
return collectFields(reqCtx, selSet, satisfies, map[string]bool{})
}

func collectFields(reqCtx *OperationContext, selSet ast.SelectionSet, satisfies []string, visited map[string]bool) []CollectedField {
groupedFields := make([]CollectedField, 0, len(selSet))

for _, sel := range selSet {
switch sel := sel.(type) {
case *ast.Field:
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) {
continue
}
f := getOrCreateAndAppendField(&groupedFields, sel.Name, sel.Alias, sel.ObjectDefinition, func() CollectedField {
return CollectedField{Field: sel}
})

f.Selections = append(f.Selections, sel.SelectionSet...)
case *ast.InlineFragment:
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) {
continue
}
if len(satisfies) > 0 && !instanceOf(sel.TypeCondition, satisfies) {
continue
}
for _, childField := range collectFields(reqCtx, sel.SelectionSet, satisfies, visited) {
f := getOrCreateAndAppendField(&groupedFields, childField.Name, childField.Alias, childField.ObjectDefinition, func() CollectedField { return childField })
f.Selections = append(f.Selections, childField.Selections...)
}

case *ast.FragmentSpread:
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) {
continue
}
fragmentName := sel.Name
if _, seen := visited[fragmentName]; seen {
continue
}
visited[fragmentName] = true

fragment := reqCtx.Doc.Fragments.ForName(fragmentName)
if fragment == nil {
// should never happen, validator has already run
panic(fmt.Errorf("missing fragment %s", fragmentName))
}

if len(satisfies) > 0 && !instanceOf(fragment.TypeCondition, satisfies) {
continue
}

for _, childField := range collectFields(reqCtx, fragment.SelectionSet, satisfies, visited) {
f := getOrCreateAndAppendField(&groupedFields, childField.Name, childField.Alias, childField.ObjectDefinition, func() CollectedField { return childField })
f.Selections = append(f.Selections, childField.Selections...)
}
default:
panic(fmt.Errorf("unsupported %T", sel))
}
}

return groupedFields
}

type CollectedField struct {
*ast.Field

Selections ast.SelectionSet
}

func instanceOf(val string, satisfies []string) bool {
for _, s := range satisfies {
if val == s {
return true
}
}
return false
}

func getOrCreateAndAppendField(c *[]CollectedField, name string, alias string, objectDefinition *ast.Definition, creator func() CollectedField) *CollectedField {
for i, cf := range *c {
if cf.Name == name && cf.Alias == alias && (cf.ObjectDefinition == objectDefinition || (cf.ObjectDefinition != nil && objectDefinition != nil && cf.ObjectDefinition.Name == objectDefinition.Name)) {
return &(*c)[i]
}
}

f := creator()

*c = append(*c, f)
return &(*c)[len(*c)-1]
}

func shouldIncludeNode(directives ast.DirectiveList, variables map[string]interface{}) bool {
if len(directives) == 0 {
return true
}

skip, include := false, true

if d := directives.ForName("skip"); d != nil {
skip = resolveIfArgument(d, variables)
}

if d := directives.ForName("include"); d != nil {
include = resolveIfArgument(d, variables)
}

return !skip && include
}

func resolveIfArgument(d *ast.Directive, variables map[string]interface{}) bool {
arg := d.Arguments.ForName("if")
if arg == nil {
panic(fmt.Sprintf("%s: argument 'if' not defined", d.Name))
}
value, err := arg.Value.Value(variables)
if err != nil {
panic(err)
}
ret, ok := value.(bool)
if !ok {
panic(fmt.Sprintf("%s: argument 'if' is not a boolean", d.Name))
}
return ret
}
104 changes: 104 additions & 0 deletions internal/graphql/fieldset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package graphql

import (
"io"
"sync"

gql "github.com/99designs/gqlgen/graphql"
)

type FieldSet struct {
fields []CollectedField
Values []gql.Marshaler
delayed []delayedResult
}

type delayedResult struct {
i int
f func() gql.Marshaler
}

func NewFieldSet(fields []CollectedField) *FieldSet {
return &FieldSet{
fields: fields,
Values: make([]gql.Marshaler, len(fields)),
}
}

func (m *FieldSet) Concurrently(i int, f func() gql.Marshaler) {
m.delayed = append(m.delayed, delayedResult{i: i, f: f})
}

func (m *FieldSet) Dispatch() {
if len(m.delayed) == 1 {
// only one concurrent task, no need to spawn a goroutine or deal create waitgroups
d := m.delayed[0]
m.Values[d.i] = d.f()
} else if len(m.delayed) > 1 {
// more than one concurrent task, use the main goroutine to do one, only spawn goroutines for the others

var wg sync.WaitGroup
for _, d := range m.delayed[1:] {
wg.Add(1)
go func(d delayedResult) {
m.Values[d.i] = d.f()
wg.Done()
}(d)
}

m.Values[m.delayed[0].i] = m.delayed[0].f()
wg.Wait()
}
}

var openBrace = []byte(`{`)
var closeBrace = []byte(`}`)
var colon = []byte(`:`)
var comma = []byte(`,`)

func (m *FieldSet) MarshalGQL(writer io.Writer) {
writer.Write(openBrace)
for i, field := range m.fields {
if i != 0 {
writer.Write(comma)
}
writeQuotedString(writer, field.Alias)
writer.Write(colon)
m.Values[i].MarshalGQL(writer)
}
writer.Write(closeBrace)
}

const encodeHex = "0123456789ABCDEF"

func writeQuotedString(w io.Writer, s string) {
start := 0
io.WriteString(w, `"`)

for i, c := range s {
if c < 0x20 || c == '\\' || c == '"' {
io.WriteString(w, s[start:i])

switch c {
case '\t':
io.WriteString(w, `\t`)
case '\r':
io.WriteString(w, `\r`)
case '\n':
io.WriteString(w, `\n`)
case '\\':
io.WriteString(w, `\\`)
case '"':
io.WriteString(w, `\"`)
default:
io.WriteString(w, `\u00`)
w.Write([]byte{encodeHex[c>>4], encodeHex[c&0xf]})
}

start = i + 1
}
}

io.WriteString(w, s[start:])
io.WriteString(w, `"`)
}
Loading