Repository: jensneuse/graphql-go-tools
Pull request #126 (2019-12-02 10:38:26)

add implementation and tests for complex nested input variable rendering
This PR will fix issues when using complex nested structures for input variables.


Patch:
diff --git a/go.mod b/go.mod
index 5bad52ae..994a36dc 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
 	github.com/pkg/errors v0.8.1 // indirect
 	github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561
 	github.com/stretchr/testify v1.4.0 // indirect
+	github.com/valyala/fasttemplate v1.1.0
 	go.uber.org/atomic v1.4.0 // indirect
 	go.uber.org/multierr v1.2.0 // indirect
 	go.uber.org/zap v1.11.0
diff --git a/go.sum b/go.sum
index da399ff5..95a34db7 100644
--- a/go.sum
+++ b/go.sum
@@ -90,6 +90,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
+github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
diff --git a/pkg/execution/execution.go b/pkg/execution/execution.go
index 95b87655..43d4f112 100644
--- a/pkg/execution/execution.go
+++ b/pkg/execution/execution.go
@@ -9,11 +9,12 @@ import (
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
 	"github.com/jensneuse/graphql-go-tools/pkg/ast"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/literal"
+	"github.com/jensneuse/graphql-go-tools/pkg/lexer/runes"
+	"github.com/valyala/fasttemplate"
 	"io"
 	"strconv"
 	"strings"
 	"sync"
-	"text/template"
 )
 
 type Executor struct {
@@ -240,69 +241,44 @@ func (e *Executor) ResolveArgs(args []Argument, data []byte) ResolvedArgs {
 		}
 	}
 
-	var offset int
-	for i := 0; i < len(resolved)-offset; i++ {
-		if bytes.Contains(resolved[i].Key, literal.DOT) {
-			for j := 0; j < len(resolved); j++ {
-				start := bytes.Index(resolved[j].Value, resolved[i].Key)
-				if start == -1 {
-					continue
-				}
-
-				end := start + bytes.Index(resolved[j].Value[start:], literal.SPACE)
-				key := append(literal.DOT,resolved.NextKey()...)
-				selector := bytes.TrimPrefix(resolved[j].Value[start:end], resolved[i].Key)
-				resolved[j].Value = append(resolved[j].Value[:start], append(key, resolved[j].Value[end:]...)...)
-
-				if len(selector) == 0 {
-					resolved = append(resolved, ResolvedArgument{
-						Key:   key,
-						Value: resolved[i].Value,
-					})
-				} else {
-					selector = bytes.TrimPrefix(selector, literal.DOT)
-					keys := strings.Split(string(selector), ".")
-					value, _, _, err := jsonparser.Get(resolved[i].Value, keys...)
-					if err != nil {
-						value = []byte(err.Error())
-					}
-					resolved = append(resolved, ResolvedArgument{
-						Key:   key,
-						Value: value,
-					})
-				}
-				offset++
-			}
-		}
-	}
-
-	vars := make(map[string]string, len(resolved))
-	for i := range resolved {
-		if !bytes.HasPrefix(resolved[i].Key,literal.DOT){
-			vars[string(resolved[i].Key)] = string(resolved[i].Value)
-			continue
-		}
-		key := resolved[i].Key[1:]
-		if bytes.Contains(key,literal.DOT){
-			continue
-		}
-		vars[string(key)] = string(resolved[i].Value)
-	}
-
 	for i := range resolved {
-		if !bytes.Contains(resolved[i].Value,[]byte("{{")){
+		if !bytes.Contains(resolved[i].Value,literal.DOUBLE_LBRACE){
 			continue
 		}
-		tmpl,err := template.New("tmpl").Parse(string(resolved[i].Value))
+		t, err := fasttemplate.NewTemplate(string(resolved[i].Value), literal.DOUBLE_LBRACE_STR, literal.DOUBLE_RBRACE_STR)
 		if err != nil {
 			continue
 		}
-		buf := bytes.Buffer{}
-		err = tmpl.Execute(&buf,vars)
-		if err != nil {
-			continue
-		}
-		resolved[i].Value = buf.Bytes()
+		value := t.ExecuteFuncString(func(w io.Writer, tag string) (i int, e error) {
+			tag = strings.TrimFunc(tag, func(r rune) bool {
+				return r == runes.SPACE || r == runes.TAB || r == runes.LINETERMINATOR
+			})
+			if strings.Count(tag,".") == 1 {
+				tag = strings.TrimPrefix(tag,".")
+				tagBytes := []byte(tag)
+				for j := range resolved {
+					if bytes.Equal(resolved[j].Key,tagBytes){
+						return w.Write(resolved[j].Value)
+					}
+				}
+			}
+			for j := range resolved {
+				key := string(resolved[j].Key)
+				if !strings.HasPrefix(tag,key){
+					continue
+				}
+				key = strings.TrimPrefix(tag,key)
+				if key == "" {
+					return w.Write(resolved[j].Value)
+				}
+				key = strings.TrimPrefix(key,".")
+				keys := strings.Split(key,".")
+				value,_,_,_ := jsonparser.Get(resolved[j].Value,keys...)
+				return w.Write(value)
+			}
+			return w.Write([]byte("{{ " + tag + " }}"))
+		})
+		resolved[i].Value = []byte(value)
 	}
 
 	resolved.Filter(func(i int) (keep bool) {
diff --git a/pkg/lexer/literal/literal.go b/pkg/lexer/literal/literal.go
index a890c15a..4f866346 100644
--- a/pkg/lexer/literal/literal.go
+++ b/pkg/lexer/literal/literal.go
@@ -27,6 +27,7 @@ var (
 	RBRACK = []byte("]")
 	LBRACE = []byte("{")
 	DOUBLE_LBRACE = []byte("{{")
+	DOUBLE_RBRACE = []byte("}}")
 	RBRACE = []byte("}")
 
 	GOBOOL    = []byte("bool")
@@ -105,6 +106,11 @@ var (
 	LocationInputFieldDefinition = []byte("INPUT_FIELD_DEFINITION")
 )
 
+const (
+	DOUBLE_LBRACE_STR = "{{"
+	DOUBLE_RBRACE_STR = "}}"
+)
+
 type Literal []byte
 
 func (l Literal) Equals(another Literal) bool {


Test patch:
diff --git a/pkg/execution/execution_test.go b/pkg/execution/execution_test.go
index f33cc3e5..8727ffdf 100644
--- a/pkg/execution/execution_test.go
+++ b/pkg/execution/execution_test.go
@@ -1230,6 +1230,52 @@ func TestExecutor_ResolveArgsString(t *testing.T) {
 	}
 }
 
+func TestExecutor_ResolveArgs_MultipleNested(t *testing.T) {
+	e := NewExecutor()
+	e.context = Context{
+		Context: context.Background(),
+		Variables: map[uint64][]byte{
+			xxhash.Sum64String("from"): []byte(`{"year":2019,"month":11,"day":1}`),
+			xxhash.Sum64String("until"): []byte(`{"year":2019,"month":12,"day":31}`),
+			xxhash.Sum64String("page"): []byte(`0`),
+		},
+	}
+
+	args := []Argument{
+		&StaticVariableArgument{
+			Name:  []byte("url"),
+			Value: []byte("/api/usage/apis/{{ .id }}/{{ .arguments.from.day }}/{{ .arguments.from.month }}/{{ .arguments.from.year }}/{{ .arguments.until.day }}/{{ .arguments.until.month }}/{{ .arguments.until.year }}?by=Hits&sort=1&p={{ .arguments.page }}"),
+		},
+		&StaticVariableArgument{
+			Name:  []byte("id"),
+			Value: []byte("1"),
+		},
+		&ContextVariableArgument{
+			Name:         []byte(".arguments.from"),
+			VariableName: []byte("from"),
+		},
+		&ContextVariableArgument{
+			Name:         []byte(".arguments.until"),
+			VariableName: []byte("until"),
+		},
+		&ContextVariableArgument{
+			Name:         []byte(".arguments.page"),
+			VariableName: []byte("page"),
+		},
+	}
+
+	resolved := e.ResolveArgs(args, nil)
+	if len(resolved) != 2 {
+		t.Fatalf("want 1, got: %d\n", len(resolved))
+		return
+	}
+	want := []byte("/api/usage/apis/1/1/11/2019/31/12/2019?by=Hits&sort=1&p=0")
+	got := resolved.ByKey([]byte("url"))
+	if !bytes.Equal(got, want) {
+		t.Fatalf("want key 'body' with value: '%s'\ngot: '%s'\ndump: %s", string(want),string(got), resolved.Dump())
+	}
+}
+
 func TestExecutor_ResolveArgsComplexPayload(t *testing.T) {
 	e := NewExecutor()
 	e.context = Context{