Skip to content

Commit

Permalink
add ExecuteStringStd and ExecuteStd for strings.Replacer drop-in repl…
Browse files Browse the repository at this point in the history
…acement (#23)
  • Loading branch information
DoubleDi authored Jul 5, 2020
1 parent 05015c3 commit 10bd7db
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 15 deletions.
89 changes: 88 additions & 1 deletion template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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 {
Expand All @@ -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))
}
}
60 changes: 59 additions & 1 deletion template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
72 changes: 59 additions & 13 deletions template_timing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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) {
Expand All @@ -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)
}
}
})
Expand All @@ -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)
}
}
})
Expand All @@ -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()
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

0 comments on commit 10bd7db

Please sign in to comment.