Explorar el Código

Initial commit

Aliaksandr Valialkin hace 10 años
commit
8de417f447
Se han modificado 6 ficheros con 684 adiciones y 0 borrados
  1. 22 0
      LICENSE
  2. 52 0
      README.md
  3. 68 0
      example_test.go
  4. 135 0
      template.go
  5. 240 0
      template_test.go
  6. 167 0
      timing_test.go

+ 22 - 0
LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Aliaksandr Valialkin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+

+ 52 - 0
README.md

@@ -0,0 +1,52 @@
+fasttemplate
+============
+
+Simple and fast template library for Go.
+
+Fasttemplate peforms only a single task - it substitutes template placeholders
+with user-defined values. At high speed :)
+
+Fasttemplate is faster than [text/template](http://golang.org/pkg/text/template/),
+[strings.Replace](http://golang.org/pkg/strings/#Replace)
+and [strings.Replacer](http://golang.org/pkg/strings/#Replacer) on placeholders'
+substitution.
+
+Below are benchmark results comparing fasttemplate performance to text/template,
+strings.Replace and strings.Replacer:
+
+```
+$ go test -bench=.
+PASS
+BenchmarkStringsReplace-4            	  500000	      2919 ns/op
+BenchmarkStringsReplacer-4           	  500000	      2632 ns/op
+BenchmarkTextTemplate-4              	  500000	      3042 ns/op
+BenchmarkFastTemplateExecute-4       	 5000000	       328 ns/op
+BenchmarkFastTemplateExecuteString-4 	 3000000	       479 ns/op
+BenchmarkFastTemplateExecuteTagFunc-4	 2000000	       667 ns/op
+```
+
+Docs
+====
+
+See http://godoc.org/github.com/valyala/fasttemplate .
+
+Usage
+=====
+
+Server:
+```go
+	template := "http://{{host}}/?q={{query}}&foo={{bar}}{{bar}}"
+	t, err := fasttemplate.NewTemplate(template, "{{", "}}")
+	if err != nil {
+		log.Fatalf("unexpected error when parsing template: %s", err)
+	}
+	s := t.ExecuteString(map[string]interface{}{
+		"host":  "google.com",
+		"query": url.QueryEscape("hello=world"),
+		"bar":   "foobar",
+	})
+	fmt.Printf("%s", s)
+
+	// Output:
+	// http://google.com/?q=hello%3Dworld&foo=foobarfoobar
+```

+ 68 - 0
example_test.go

@@ -0,0 +1,68 @@
+package fasttemplate
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"net/url"
+)
+
+func ExampleTemplate() {
+	template := "http://{{host}}/?foo={{bar}}{{bar}}&q={{query}}&baz={{baz}}"
+	t, err := NewTemplate(template, "{{", "}}")
+	if err != nil {
+		log.Fatalf("unexpected error when parsing template: %s", err)
+	}
+
+	// Substitution map.
+	// Since "baz" tag is missing in the map, it will be substituted
+	// by an empty string.
+	m := map[string]interface{}{
+		"host": "google.com",     // string - convenient
+		"bar":  []byte("foobar"), // byte slice - the fastest
+
+		// TagFunc - flexible value. TagFunc is called only if the given
+		// tag exists in the template.
+		"query": TagFunc(func(w io.Writer) (int, error) {
+			return w.Write([]byte(url.QueryEscape("hello=world")))
+		}),
+	}
+
+	s := t.ExecuteString(m)
+	fmt.Printf("%s", s)
+
+	// Output:
+	// http://google.com/?foo=foobarfoobar&q=hello%3Dworld&baz=
+}
+
+func ExampleTagFunc() {
+	template := "foo[baz]bar"
+	t, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		log.Fatalf("unexpected error when parsing template: %s", err)
+	}
+
+	bazSlice := [][]byte{[]byte("123"), []byte("456"), []byte("789")}
+	m := map[string]interface{}{
+		// Always wrap the function into TagFunc.
+		//
+		// "baz" tag function writes bazSlice contents into w.
+		"baz": TagFunc(func(w io.Writer) (int, error) {
+			var nn int
+			for _, x := range bazSlice {
+				n, err := w.Write(x)
+				if err != nil {
+					return nn, err
+				}
+				nn += n
+			}
+			return nn, nil
+		}),
+	}
+
+	s := t.ExecuteString(m)
+	fmt.Printf("%s", s)
+
+	// Output:
+	// foo123456789bar
+}

+ 135 - 0
template.go

@@ -0,0 +1,135 @@
+package fasttemplate
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"sync"
+)
+
+// Template implements simple template engine, which can be used for fast
+// tags (aka placeholders) substitution.
+type Template struct {
+	texts           [][]byte
+	tags            []string
+	bytesBufferPool sync.Pool
+}
+
+// NewTemplate parses the given template using the given startTag and endTag
+// as tag start and tag end.
+//
+// The returned template can be executed by concurrently running goroutines
+// using Execute* methods.
+func NewTemplate(template, startTag, endTag string) (*Template, error) {
+	var t Template
+
+	if len(startTag) == 0 {
+		panic("startTag cannot be empty")
+	}
+	if len(endTag) == 0 {
+		panic("endTag cannot be empty")
+	}
+
+	s := []byte(template)
+	a := []byte(startTag)
+	b := []byte(endTag)
+
+	for {
+		n := bytes.Index(s, a)
+		if n < 0 {
+			t.texts = append(t.texts, s)
+			break
+		}
+		t.texts = append(t.texts, s[:n])
+
+		s = s[n+len(a):]
+		n = bytes.Index(s, b)
+		if n < 0 {
+			return nil, fmt.Errorf("Cannot find end tag=%q in the template=%q starting from %q", endTag, template, s)
+		}
+
+		t.tags = append(t.tags, string(s[:n]))
+		s = s[n+len(b):]
+	}
+
+	return &t, nil
+}
+
+// TagFunc can be used as substitution value in the map passed to Execute*.
+//
+// It must write contents to w and return the number of bytes written.
+type TagFunc func(w io.Writer) (int, error)
+
+// Execute substitutes template tags (placeholders) with the corresponding
+// values from the map m and writes the result to the given writer w.
+//
+// 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) Execute(w io.Writer, m map[string]interface{}) (int64, error) {
+	var nn int64
+
+	n := len(t.texts) - 1
+	for i := 0; i < n; i++ {
+		ni, err := w.Write(t.texts[i])
+		if err != nil {
+			return nn, err
+		}
+		nn += int64(ni)
+
+		k := t.tags[i]
+		v := m[k]
+		if v == nil {
+			continue
+		}
+		switch value := v.(type) {
+		case []byte:
+			ni, err = w.Write(value)
+		case string:
+			ni, err = w.Write([]byte(value))
+		case TagFunc:
+			ni, err = value(w)
+		default:
+			panic(fmt.Sprintf("key=%q contains unexpected value type=%#v. Expected []byte, string or TagFunc", k, v))
+		}
+		if err != nil {
+			return nn, err
+		}
+		nn += int64(ni)
+	}
+	ni, err := w.Write(t.texts[n])
+	if err != nil {
+		return nn, err
+	}
+	nn += int64(ni)
+	return nn, nil
+}
+
+// ExecuteString substitutes template tags (placeholders) with the corresponding
+// values from the map m and returns the result.
+//
+// Substitution map m may contain values with the following types:
+//   * []byte - the fastest value type
+//   * string - convenient value type
+//   * TagFunc - flexible value type
+//
+func (t *Template) ExecuteString(m map[string]interface{}) string {
+	var w *bytes.Buffer
+	wv := t.bytesBufferPool.Get()
+	if wv == nil {
+		w = &bytes.Buffer{}
+	} else {
+		w = wv.(*bytes.Buffer)
+	}
+	_, err := t.Execute(w, m)
+	if err != nil {
+		panic(fmt.Sprintf("unexpected error: %s", err))
+	}
+	s := string(w.Bytes())
+	w.Reset()
+	t.bytesBufferPool.Put(w)
+	return s
+}

+ 240 - 0
template_test.go

@@ -0,0 +1,240 @@
+package fasttemplate
+
+import (
+	"io"
+	"testing"
+)
+
+func TestEmptyTemplate(t *testing.T) {
+	tpl, err := NewTemplate("", "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "bar", "aaa": "bbb"})
+	if s != "" {
+		t.Fatalf("unexpected string returned %q. Expected empty string", s)
+	}
+}
+
+func TestEmptyTagStart(t *testing.T) {
+	expectPanic(t, func() { NewTemplate("foobar", "", "]") })
+}
+
+func TestEmptyTagEnd(t *testing.T) {
+	expectPanic(t, func() { NewTemplate("foobar", "[", "") })
+}
+
+func TestNoTags(t *testing.T) {
+	template := "foobar"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "bar", "aaa": "bbb"})
+	if s != template {
+		t.Fatalf("unexpected template value %q. Expected %q", s, template)
+	}
+}
+
+func TestEmptyTagName(t *testing.T) {
+	template := "foo[]bar"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"": "111", "aaa": "bbb"})
+	result := "foo111bar"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestOnlyTag(t *testing.T) {
+	template := "[foo]"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"})
+	result := "111"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestStartWithTag(t *testing.T) {
+	template := "[foo]barbaz"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"})
+	result := "111barbaz"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestEndWithTag(t *testing.T) {
+	template := "foobar[foo]"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"})
+	result := "foobar111"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestDuplicateTags(t *testing.T) {
+	template := "[foo]bar[foo][foo]baz"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"})
+	result := "111bar111111baz"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestMultipleTags(t *testing.T) {
+	template := "foo[foo]aa[aaa]ccc"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"})
+	result := "foo111aabbbccc"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestLongDelimiter(t *testing.T) {
+	template := "foo{{{foo}}}bar"
+	tpl, err := NewTemplate(template, "{{{", "}}}")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"})
+	result := "foo111bar"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestIdenticalDelimiter(t *testing.T) {
+	template := "foo@foo@foo@aaa@"
+	tpl, err := NewTemplate(template, "@", "@")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "111", "aaa": "bbb"})
+	result := "foo111foobbb"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestDlimitersWithDistinctSize(t *testing.T) {
+	template := "foo<?phpaaa?>bar<?phpzzz?>"
+	tpl, err := NewTemplate(template, "<?php", "?>")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"zzz": "111", "aaa": "bbb"})
+	result := "foobbbbar111"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestEmptyValue(t *testing.T) {
+	template := "foobar[foo]"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"foo": "", "aaa": "bbb"})
+	result := "foobar"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestNoValue(t *testing.T) {
+	template := "foobar[foo]x[aaa]"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{"aaa": "bbb"})
+	result := "foobarxbbb"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func TestNoEndDelimiter(t *testing.T) {
+	template := "foobar[foo"
+	_, err := NewTemplate(template, "[", "]")
+	if err == nil {
+		t.Fatalf("expected non-nil error. got nil")
+	}
+}
+
+func TestUnsupportedValue(t *testing.T) {
+	template := "foobar[foo]"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	expectPanic(t, func() {
+		tpl.ExecuteString(map[string]interface{}{"foo": 123, "aaa": "bbb"})
+	})
+}
+
+func TestMixedValues(t *testing.T) {
+	template := "foo[foo]bar[bar]baz[baz]"
+	tpl, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		t.Fatalf("unexpected error: %s", err)
+	}
+
+	s := tpl.ExecuteString(map[string]interface{}{
+		"foo": "111",
+		"bar": []byte("bbb"),
+		"baz": TagFunc(func(w io.Writer) (int, error) { return w.Write([]byte("zzz")) }),
+	})
+	result := "foo111barbbbbazzzz"
+	if s != result {
+		t.Fatalf("unexpected template value %q. Expected %q", s, result)
+	}
+}
+
+func expectPanic(t *testing.T, f func()) {
+	defer func() {
+		if r := recover(); r == nil {
+			t.Fatalf("missing panic")
+		}
+	}()
+	f()
+}

+ 167 - 0
timing_test.go

@@ -0,0 +1,167 @@
+package fasttemplate
+
+import (
+	"bytes"
+	"io"
+	"net/url"
+	"strings"
+	"testing"
+	"text/template"
+)
+
+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"
+
+	resultBytes        = []byte(result)
+	resultEscapedBytes = []byte(resultEscaped)
+
+	m = map[string]interface{}{
+		"cb":      []byte("1234"),
+		"width":   []byte("1232"),
+		"height":  []byte("123"),
+		"timeout": []byte("123123"),
+		"uid":     []byte("aaasdf"),
+		"subid":   []byte("asdfds"),
+		"ref":     []byte("http://google.com/aaa/bbb/ccc"),
+	}
+)
+
+func map2slice(m map[string]interface{}) []string {
+	var a []string
+	for k, v := range m {
+		a = append(a, "{{"+k+"}}", string(v.([]byte)))
+	}
+	return a
+}
+
+func BenchmarkStringsReplace(b *testing.B) {
+	mSlice := map2slice(m)
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			x := source
+			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)
+			}
+		}
+	})
+}
+
+func BenchmarkStringsReplacer(b *testing.B) {
+	mSlice := map2slice(m)
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		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)
+			}
+		}
+	})
+}
+
+func BenchmarkTextTemplate(b *testing.B) {
+	s := strings.Replace(source, "{{", "{{.", -1)
+	t, err := template.New("test").Parse(s)
+	if err != nil {
+		b.Fatalf("Error when parsing template: %s", err)
+	}
+
+	mm := make(map[string]string)
+	for k, v := range m {
+		mm[k] = string(v.([]byte))
+	}
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		var w bytes.Buffer
+		for pb.Next() {
+			if err := t.Execute(&w, mm); err != nil {
+				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)
+			}
+			w.Reset()
+		}
+	})
+}
+
+func BenchmarkFastTemplateExecute(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.Execute(&w, m); err != nil {
+				b.Fatalf("unexpected error: %s", err)
+			}
+			x := w.Bytes()
+			if !bytes.Equal(x, resultBytes) {
+				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultBytes)
+			}
+			w.Reset()
+		}
+	})
+}
+
+func BenchmarkFastTemplateExecuteString(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.ExecuteString(m)
+			if x != result {
+				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, result)
+			}
+		}
+	})
+}
+
+func BenchmarkFastTemplateExecuteTagFunc(b *testing.B) {
+	t, err := NewTemplate(source, "{{", "}}")
+	if err != nil {
+		b.Fatalf("error in template: %s", err)
+	}
+
+	mm := make(map[string]interface{})
+	for k, v := range m {
+		if k == "ref" {
+			vv := v.([]byte)
+			v = TagFunc(func(w io.Writer) (int, error) { return w.Write([]byte(url.QueryEscape(string(vv)))) })
+		}
+		mm[k] = v
+	}
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		var w bytes.Buffer
+		for pb.Next() {
+			if _, err := t.Execute(&w, mm); err != nil {
+				b.Fatalf("unexpected error: %s", err)
+			}
+			x := w.Bytes()
+			if !bytes.Equal(x, resultEscapedBytes) {
+				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, resultEscapedBytes)
+			}
+			w.Reset()
+		}
+	})
+}