Browse Source

add ExecuteStringStd and ExecuteStd for strings.Replacer drop-in replacement (#23)

Denis 5 years ago
parent
commit
10bd7db1dd
3 changed files with 206 additions and 15 deletions
  1. 88 1
      template.go
  2. 59 1
      template_test.go
  3. 59 13
      template_timing_test.go

+ 88 - 1
template.go

@@ -9,8 +9,9 @@ package fasttemplate
 import (
 	"bytes"
 	"fmt"
-	"github.com/valyala/bytebufferpool"
 	"io"
+
+	"github.com/valyala/bytebufferpool"
 )
 
 // ExecuteFunc calls f on each template tag (placeholder) occurrence.
@@ -76,6 +77,22 @@ func Execute(template, startTag, endTag string, w io.Writer, m map[string]interf
 	return ExecuteFunc(template, startTag, endTag, w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
 }
 
+// ExecuteStd works the same way as Execute, but keeps the unknown placeholders.
+// This can be used as a drop-in replacement for strings.Replacer
+//
+// Substitution map m may contain values with the following types:
+//   * []byte - the fastest value type
+//   * string - convenient value type
+//   * TagFunc - flexible value type
+//
+// Returns the number of bytes written to w.
+//
+// This function is optimized for constantly changing templates.
+// Use Template.ExecuteStd for frozen templates.
+func ExecuteStd(template, startTag, endTag string, w io.Writer, m map[string]interface{}) (int64, error) {
+	return ExecuteFunc(template, startTag, endTag, w, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, startTag, endTag, tag, m) })
+}
+
 // ExecuteFuncString calls f on each template tag (placeholder) occurrence
 // and substitutes it with the data written to TagFunc's w.
 //
@@ -128,6 +145,20 @@ func ExecuteString(template, startTag, endTag string, m map[string]interface{})
 	return ExecuteFuncString(template, startTag, endTag, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
 }
 
+// ExecuteStringStd works the same way as ExecuteString, but keeps the unknown placeholders.
+// This can be used as a drop-in replacement for strings.Replacer
+//
+// Substitution map m may contain values with the following types:
+//   * []byte - the fastest value type
+//   * string - convenient value type
+//   * TagFunc - flexible value type
+//
+// This function is optimized for constantly changing templates.
+// Use Template.ExecuteStringStd for frozen templates.
+func ExecuteStringStd(template, startTag, endTag string, m map[string]interface{}) string {
+	return ExecuteFuncString(template, startTag, endTag, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, startTag, endTag, tag, m) })
+}
+
 // Template implements simple template engine, which can be used for fast
 // tags' (aka placeholders) substitution.
 type Template struct {
@@ -283,6 +314,19 @@ func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error)
 	return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
 }
 
+// ExecuteStd works the same way as Execute, but keeps the unknown placeholders.
+// This can be used as a drop-in replacement for strings.Replacer
+//
+// Substitution map m may contain values with the following types:
+//   * []byte - the fastest value type
+//   * string - convenient value type
+//   * TagFunc - flexible value type
+//
+// Returns the number of bytes written to w.
+func (t *Template) ExecuteStd(w io.Writer, m map[string]interface{}) (int64, error) {
+	return t.ExecuteFunc(w, func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) })
+}
+
 // ExecuteFuncString calls f on each template tag (placeholder) occurrence
 // and substitutes it with the data written to TagFunc's w.
 //
@@ -332,6 +376,20 @@ func (t *Template) ExecuteString(m map[string]interface{}) string {
 	return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
 }
 
+// ExecuteStringStd works the same way as ExecuteString, but keeps the unknown placeholders.
+// This can be used as a drop-in replacement for strings.Replacer
+//
+// Substitution map m may contain values with the following types:
+//   * []byte - the fastest value type
+//   * string - convenient value type
+//   * TagFunc - flexible value type
+//
+// This function is optimized for frozen templates.
+// Use ExecuteStringStd for constantly changing templates.
+func (t *Template) ExecuteStringStd(m map[string]interface{}) string {
+	return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return keepUnknownTagFunc(w, t.startTag, t.endTag, tag, m) })
+}
+
 func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) {
 	v := m[tag]
 	if v == nil {
@@ -348,3 +406,32 @@ func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error)
 		panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v))
 	}
 }
+
+func keepUnknownTagFunc(w io.Writer, startTag, endTag, tag string, m map[string]interface{}) (int, error) {
+	v, ok := m[tag]
+	if !ok {
+		if _, err := w.Write(unsafeString2Bytes(startTag)); err != nil {
+			return 0, err
+		}
+		if _, err := w.Write(unsafeString2Bytes(tag)); err != nil {
+			return 0, err
+		}
+		if _, err := w.Write(unsafeString2Bytes(endTag)); err != nil {
+			return 0, err
+		}
+		return len(startTag) + len(tag) + len(endTag), nil
+	}
+	if v == nil {
+		return 0, nil
+	}
+	switch value := v.(type) {
+	case []byte:
+		return w.Write(value)
+	case string:
+		return w.Write([]byte(value))
+	case TagFunc:
+		return value(w, tag)
+	default:
+		panic(fmt.Sprintf("tag=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", tag, v))
+	}
+}

+ 59 - 1
template_test.go

@@ -280,6 +280,36 @@ func testExecute(t *testing.T, template, expectedOutput string) {
 	}
 }
 
+func TestExecuteStd(t *testing.T) {
+	testExecuteStd(t, "", "")
+	testExecuteStd(t, "a", "a")
+	testExecuteStd(t, "abc", "abc")
+	testExecuteStd(t, "{foo}", "xxxx")
+	testExecuteStd(t, "a{foo}", "axxxx")
+	testExecuteStd(t, "{foo}a", "xxxxa")
+	testExecuteStd(t, "a{foo}bc", "axxxxbc")
+	testExecuteStd(t, "{foo}{foo}", "xxxxxxxx")
+	testExecuteStd(t, "{foo}bar{foo}", "xxxxbarxxxx")
+
+	// unclosed tag
+	testExecuteStd(t, "{unclosed", "{unclosed")
+	testExecuteStd(t, "{{unclosed", "{{unclosed")
+	testExecuteStd(t, "{un{closed", "{un{closed")
+
+	// test unknown tag
+	testExecuteStd(t, "{unknown}", "{unknown}")
+	testExecuteStd(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx")
+}
+
+func testExecuteStd(t *testing.T, template, expectedOutput string) {
+	var bb bytes.Buffer
+	ExecuteStd(template, "{", "}", &bb, map[string]interface{}{"foo": "xxxx"})
+	output := string(bb.Bytes())
+	if output != expectedOutput {
+		t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput)
+	}
+}
+
 func TestExecuteString(t *testing.T) {
 	testExecuteString(t, "", "")
 	testExecuteString(t, "a", "a")
@@ -308,6 +338,34 @@ func testExecuteString(t *testing.T, template, expectedOutput string) {
 	}
 }
 
+func TestExecuteStringStd(t *testing.T) {
+	testExecuteStringStd(t, "", "")
+	testExecuteStringStd(t, "a", "a")
+	testExecuteStringStd(t, "abc", "abc")
+	testExecuteStringStd(t, "{foo}", "xxxx")
+	testExecuteStringStd(t, "a{foo}", "axxxx")
+	testExecuteStringStd(t, "{foo}a", "xxxxa")
+	testExecuteStringStd(t, "a{foo}bc", "axxxxbc")
+	testExecuteStringStd(t, "{foo}{foo}", "xxxxxxxx")
+	testExecuteStringStd(t, "{foo}bar{foo}", "xxxxbarxxxx")
+
+	// unclosed tag
+	testExecuteStringStd(t, "{unclosed", "{unclosed")
+	testExecuteStringStd(t, "{{unclosed", "{{unclosed")
+	testExecuteStringStd(t, "{un{closed", "{un{closed")
+
+	// test unknown tag
+	testExecuteStringStd(t, "{unknown}", "{unknown}")
+	testExecuteStringStd(t, "{foo}q{unexpected}{missing}bar{foo}", "xxxxq{unexpected}{missing}barxxxx")
+}
+
+func testExecuteStringStd(t *testing.T, template, expectedOutput string) {
+	output := ExecuteStringStd(template, "{", "}", map[string]interface{}{"foo": "xxxx"})
+	if output != expectedOutput {
+		t.Fatalf("unexpected output for template=%q: %q. Expected %q", template, output, expectedOutput)
+	}
+}
+
 func expectPanic(t *testing.T, f func()) {
 	defer func() {
 		if r := recover(); r == nil {
@@ -375,4 +433,4 @@ func TestTpl_ExecuteFuncStringWithErr(t *testing.T) {
 	if result != "Alice is Bob's best friend" {
 		t.Fatalf("expect: %s, but: %s", "Alice is Bob's best friend", result)
 	}
-}
+}

+ 59 - 13
template_timing_test.go

@@ -11,12 +11,16 @@ import (
 )
 
 var (
-	source        = "http://{{uid}}.foo.bar.com/?cb={{cb}}{{width}}&width={{width}}&height={{height}}&timeout={{timeout}}&uid={{uid}}&subid={{subid}}&ref={{ref}}"
-	result        = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc"
-	resultEscaped = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http%3A%2F%2Fgoogle.com%2Faaa%2Fbbb%2Fccc"
+	source             = "http://{{uid}}.foo.bar.com/?cb={{cb}}{{width}}&width={{width}}&height={{height}}&timeout={{timeout}}&uid={{uid}}&subid={{subid}}&ref={{ref}}&empty={{empty}}"
+	result             = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty="
+	resultEscaped      = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http%3A%2F%2Fgoogle.com%2Faaa%2Fbbb%2Fccc&empty="
+	resultStd          = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty={{empty}}"
+	resultTextTemplate = "http://aaasdf.foo.bar.com/?cb=12341232&width=1232&height=123&timeout=123123&uid=aaasdf&subid=asdfds&ref=http://google.com/aaa/bbb/ccc&empty=<no value>"
 
-	resultBytes        = []byte(result)
-	resultEscapedBytes = []byte(resultEscaped)
+	resultBytes             = []byte(result)
+	resultEscapedBytes      = []byte(resultEscaped)
+	resultStdBytes          = []byte(resultStd)
+	resultTextTemplateBytes = []byte(resultTextTemplate)
 
 	m = map[string]interface{}{
 		"cb":      []byte("1234"),
@@ -42,7 +46,7 @@ func BenchmarkFmtFprintf(b *testing.B) {
 		var w bytes.Buffer
 		for pb.Next() {
 			fmt.Fprintf(&w,
-				"http://%[5]s.foo.bar.com/?cb=%[1]s%[2]s&width=%[2]s&height=%[3]s&timeout=%[4]s&uid=%[5]s&subid=%[6]s&ref=%[7]s",
+				"http://%[5]s.foo.bar.com/?cb=%[1]s%[2]s&width=%[2]s&height=%[3]s&timeout=%[4]s&uid=%[5]s&subid=%[6]s&ref=%[7]s&empty=",
 				m["cb"], m["width"], m["height"], m["timeout"], m["uid"], m["subid"], m["ref"])
 			x := w.Bytes()
 			if !bytes.Equal(x, resultBytes) {
@@ -63,8 +67,8 @@ func BenchmarkStringsReplace(b *testing.B) {
 			for i := 0; i < len(mSlice); i += 2 {
 				x = strings.Replace(x, mSlice[i], mSlice[i+1], -1)
 			}
-			if x != result {
-				b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, result)
+			if x != resultStd {
+				b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, resultStd)
 			}
 		}
 	})
@@ -78,8 +82,8 @@ func BenchmarkStringsReplacer(b *testing.B) {
 		for pb.Next() {
 			r := strings.NewReplacer(mSlice...)
 			x := r.Replace(source)
-			if x != result {
-				b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, result)
+			if x != resultStd {
+				b.Fatalf("Unexpected result\n%q\nExpected\n%q\n", x, resultStd)
 			}
 		}
 	})
@@ -105,8 +109,8 @@ func BenchmarkTextTemplate(b *testing.B) {
 				b.Fatalf("error when executing template: %s", err)
 			}
 			x := w.Bytes()
-			if !bytes.Equal(x, resultBytes) {
-				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultBytes)
+			if !bytes.Equal(x, resultTextTemplateBytes) {
+				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultTextTemplateBytes)
 			}
 			w.Reset()
 		}
@@ -157,6 +161,28 @@ func BenchmarkFastTemplateExecute(b *testing.B) {
 	})
 }
 
+func BenchmarkFastTemplateExecuteStd(b *testing.B) {
+	t, err := NewTemplate(source, "{{", "}}")
+	if err != nil {
+		b.Fatalf("error in template: %s", err)
+	}
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		var w bytes.Buffer
+		for pb.Next() {
+			if _, err := t.ExecuteStd(&w, m); err != nil {
+				b.Fatalf("unexpected error: %s", err)
+			}
+			x := w.Bytes()
+			if !bytes.Equal(x, resultStdBytes) {
+				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultStdBytes)
+			}
+			w.Reset()
+		}
+	})
+}
+
 func BenchmarkFastTemplateExecuteFuncString(b *testing.B) {
 	t, err := NewTemplate(source, "{{", "}}")
 	if err != nil {
@@ -191,6 +217,23 @@ func BenchmarkFastTemplateExecuteString(b *testing.B) {
 	})
 }
 
+func BenchmarkFastTemplateExecuteStringStd(b *testing.B) {
+	t, err := NewTemplate(source, "{{", "}}")
+	if err != nil {
+		b.Fatalf("error in template: %s", err)
+	}
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			x := t.ExecuteStringStd(m)
+			if x != resultStd {
+				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultStd)
+			}
+		}
+	})
+}
+
 func BenchmarkFastTemplateExecuteTagFunc(b *testing.B) {
 	t, err := NewTemplate(source, "{{", "}}")
 	if err != nil {
@@ -262,5 +305,8 @@ func BenchmarkExecuteFunc(b *testing.B) {
 }
 
 func testTagFunc(w io.Writer, tag string) (int, error) {
-	return w.Write(m[tag].([]byte))
+	if t, ok := m[tag]; ok {
+		return w.Write(t.([]byte))
+	}
+	return 0, nil
 }