Repository: jensneuse/graphql-go-tools
Pull request #139 (2019-12-08 12:45:05)

extract escaping in own package
Initially I've implemented escaping for the execution package using the std library's json package.
This PR extracts the necessary logic into its own package and implements it in a performant way.


Patch:
diff --git a/pkg/escape/bytes.go b/pkg/escape/bytes.go
new file mode 100644
index 00000000..aae28db8
--- /dev/null
+++ b/pkg/escape/bytes.go
@@ -0,0 +1,22 @@
+package escape
+
+func Bytes(in, out []byte) []byte {
+
+	out = out[:0]
+
+	for i := range in {
+
+		switch in[i] {
+		case 9:
+			out = append(out, 92, 116) // \t
+		case 10:
+			out = append(out, 92, 110) // \n
+		case 34:
+			out = append(out, 92, 34) // \"
+		default:
+			out = append(out, in[i])
+		}
+	}
+
+	return out
+}
diff --git a/pkg/execution/execution.go b/pkg/execution/execution.go
index 42aa6daf..5328c031 100644
--- a/pkg/execution/execution.go
+++ b/pkg/execution/execution.go
@@ -8,6 +8,7 @@ import (
 	"github.com/cespare/xxhash"
 	"github.com/jensneuse/graphql-go-tools/internal/pkg/unsafebytes"
 	"github.com/jensneuse/graphql-go-tools/pkg/ast"
+	"github.com/jensneuse/graphql-go-tools/pkg/escape"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/literal"
 	"github.com/jensneuse/graphql-go-tools/pkg/lexer/runes"
 	"github.com/tidwall/gjson"
@@ -24,6 +25,7 @@ type Executor struct {
 	err          error
 	buffers      LockableBufferMap
 	instructions []Instruction
+	escapeBuf    [48]byte
 }
 
 type LockableBufferMap struct {
@@ -165,7 +167,8 @@ func (e *Executor) resolveNode(node Node, data []byte, path string, prefetch *sy
 		if node.QuoteValue {
 			e.write(literal.QUOTE)
 		}
-		e.write(e.escape(data))
+		data = escape.Bytes(data,e.escapeBuf[:0])
+		e.write(data)
 		if node.QuoteValue {
 			e.write(literal.QUOTE)
 		}
@@ -235,28 +238,6 @@ func (e *Executor) resolveNode(node Node, data []byte, path string, prefetch *sy
 	}
 }
 
-func (e *Executor) escape(data []byte) []byte {
-	if !e.dataNeedsEscaping(data) {
-		return data
-	}
-	out, err := json.Marshal(unsafebytes.BytesToString(data))
-	if err != nil {
-		panic(err)
-	}
-	out = out[1 : len(out)-1]
-	return out
-}
-
-func (e *Executor) dataNeedsEscaping(data []byte) bool {
-	for i := range data {
-		switch data[i] {
-		case runes.BACKSLASH, runes.TAB, runes.SPACE, runes.LINETERMINATOR:
-			return true
-		}
-	}
-	return false
-}
-
 func (e *Executor) ResolveArgs(args []Argument, data []byte) ResolvedArgs {
 
 	args = append(args, e.context.ExtraArguments...)


Test patch:
diff --git a/pkg/escape/bytes_test.go b/pkg/escape/bytes_test.go
new file mode 100644
index 00000000..3f87d6f7
--- /dev/null
+++ b/pkg/escape/bytes_test.go
@@ -0,0 +1,70 @@
+package escape
+
+import (
+	"bytes"
+	"encoding/json"
+	"testing"
+)
+
+func TestBytes(t *testing.T) {
+	input := `foo
+	bar
+  baz	bal
+"str"
+`
+
+	marshalled,err := json.Marshal(input)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := marshalled[1:len(marshalled)-1]
+
+	var out []byte
+
+	got := Bytes([]byte(input),out)
+	if !bytes.Equal(got,want){
+		t.Fatalf("\n%+v (want: %d)\n%+v (got: %d)\n%s (wantString)\n%s (gotString)",want,len(want),got,len(got),string(want),string(got))
+	}
+
+	out = make([]byte,len(input))
+
+	got = Bytes([]byte(input),out)
+	if !bytes.Equal(got,want){
+		t.Fatalf("\n%+v (want: %d)\n%+v (got: %d)\n%s (wantString)\n%s (gotString)",want,len(want),got,len(got),string(want),string(got))
+	}
+
+	out = out[:0]
+
+	got = Bytes([]byte(input),out)
+	if !bytes.Equal(got,want){
+		t.Fatalf("\n%+v (want: %d)\n%+v (got: %d)\n%s (wantString)\n%s (gotString)",want,len(want),got,len(got),string(want),string(got))
+	}
+
+	got = Bytes([]byte(input),out)
+	if !bytes.Equal(got,want){
+		t.Fatalf("\n%+v (want: %d)\n%+v (got: %d)\n%s (wantString)\n%s (gotString)",want,len(want),got,len(got),string(want),string(got))
+	}
+}
+
+func BenchmarkBytes(b *testing.B) {
+	input := `foo
+	bar
+  baz	bal
+`
+	inputBytes := []byte(input)
+	out := make([]byte,len(inputBytes) * 2)
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	for i := 0;i<b.N;i++{
+		if len(inputBytes) != len(input){
+			b.Fatalf("must be same len")
+		}
+		out = Bytes(inputBytes,out)
+		if len(out) == 0 {
+			b.Fatalf("must not be 0")
+		}
+	}
+}
\ No newline at end of file
diff --git a/pkg/execution/execution_test.go b/pkg/execution/execution_test.go
index 876d6883..d679c763 100644
--- a/pkg/execution/execution_test.go
+++ b/pkg/execution/execution_test.go
@@ -2160,4 +2160,4 @@ func TestExecutor_Introspection(t *testing.T) {
 
 		diffview.NewGoland().DiffViewBytes("execution", fixture, response)
 	}
-}
+}
\ No newline at end of file