Browse Source

Added ExecuteFunc*() functions, which can be used as basic blocks for 'advanced' template engine :)

Aliaksandr Valialkin 10 years ago
parent
commit
49bbb1bb04
5 changed files with 175 additions and 53 deletions
  1. 37 8
      README.md
  2. 26 4
      example_test.go
  3. 62 38
      template.go
  4. 2 2
      template_test.go
  5. 48 1
      timing_test.go

+ 37 - 8
README.md

@@ -15,25 +15,28 @@ Below are benchmark results comparing fasttemplate performance to text/template,
 strings.Replace and strings.Replacer:
 
 ```
-$ go test -bench=.
+$ go test -bench=. -benchmem
 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
+BenchmarkStringsReplace-4               	  500000	      2889 ns/op	    1824 B/op	      14 allocs/op
+BenchmarkStringsReplacer-4              	  500000	      2700 ns/op	    2256 B/op	      23 allocs/op
+BenchmarkTextTemplate-4                 	  500000	      3089 ns/op	     336 B/op	      19 allocs/op
+BenchmarkFastTemplateExecuteFunc-4      	 5000000	       333 ns/op	       0 B/op	       0 allocs/op
+BenchmarkFastTemplateExecute-4          	 5000000	       381 ns/op	       0 B/op	       0 allocs/op
+BenchmarkFastTemplateExecuteFuncString-4	 3000000	       508 ns/op	     144 B/op	       1 allocs/op
+BenchmarkFastTemplateExecuteString-4    	 3000000	       552 ns/op	     144 B/op	       1 allocs/op
+BenchmarkFastTemplateExecuteTagFunc-4   	 2000000	       709 ns/op	     144 B/op	       3 allocs/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, "{{", "}}")
@@ -50,3 +53,29 @@ Server:
 	// Output:
 	// http://google.com/?q=hello%3Dworld&foo=foobarfoobar
 ```
+
+
+Advanced usage
+==============
+
+```go
+	template := "Hello, [user]! You won [prize]!!! [foobar]"
+	t, err := fasttemplate.NewTemplate(template, "[", "]")
+	if err != nil {
+		log.Fatalf("unexpected error when parsing template: %s", err)
+	}
+	s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
+		switch tag {
+		case "user":
+			return w.Write([]byte("John"))
+		case "prize":
+			return w.Write([]byte("$100500"))
+		default:
+			return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag)))
+		}
+	})
+	fmt.Printf("%s", s)
+
+	// Output:
+	// Hello, John! You won $100500!!! [unknown tag "foobar"]
+```

+ 26 - 4
example_test.go

@@ -23,8 +23,8 @@ func ExampleTemplate() {
 
 		// 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")))
+		"query": TagFunc(func(w io.Writer, tag string) (int, error) {
+			return w.Write([]byte(url.QueryEscape(tag + "=world")))
 		}),
 	}
 
@@ -32,7 +32,7 @@ func ExampleTemplate() {
 	fmt.Printf("%s", s)
 
 	// Output:
-	// http://google.com/?foo=foobarfoobar&q=hello%3Dworld&baz=
+	// http://google.com/?foo=foobarfoobar&q=query%3Dworld&baz=
 }
 
 func ExampleTagFunc() {
@@ -47,7 +47,7 @@ func ExampleTagFunc() {
 		// Always wrap the function into TagFunc.
 		//
 		// "baz" tag function writes bazSlice contents into w.
-		"baz": TagFunc(func(w io.Writer) (int, error) {
+		"baz": TagFunc(func(w io.Writer, tag string) (int, error) {
 			var nn int
 			for _, x := range bazSlice {
 				n, err := w.Write(x)
@@ -66,3 +66,25 @@ func ExampleTagFunc() {
 	// Output:
 	// foo123456789bar
 }
+
+func ExampleTemplate_ExecuteFuncString() {
+	template := "Hello, [user]! You won [prize]!!! [foobar]"
+	t, err := NewTemplate(template, "[", "]")
+	if err != nil {
+		log.Fatalf("unexpected error when parsing template: %s", err)
+	}
+	s := t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) {
+		switch tag {
+		case "user":
+			return w.Write([]byte("John"))
+		case "prize":
+			return w.Write([]byte("$100500"))
+		default:
+			return w.Write([]byte(fmt.Sprintf("[unknown tag %q]", tag)))
+		}
+	})
+	fmt.Printf("%s", s)
+
+	// Output:
+	// Hello, John! You won $100500!!! [unknown tag "foobar"]
+}

+ 62 - 38
template.go

@@ -1,3 +1,9 @@
+// Package fasttemplate implements simple and fast template library.
+//
+// Fasttemplate is faster than text/template, strings.Replace
+// and strings.Replacer.
+//
+// Fasttemplate ideally fits for fast and simple placeholders' substitutions.
 package fasttemplate
 
 import (
@@ -52,24 +58,26 @@ func NewTemplate(template, startTag, endTag string) (*Template, error) {
 		s = s[n+len(b):]
 	}
 
+	t.bytesBufferPool.New = newBytesBuffer
 	return &t, nil
 }
 
+func newBytesBuffer() interface{} {
+	return &bytes.Buffer{}
+}
+
 // TagFunc can be used as a substitution value in the map passed to Execute*.
+// Execute* functions passes tag (placeholder) name in 'tag' argument.
 //
-// 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.
+// TagFunc must be safe to call from concurrently running goroutines.
 //
-// Substitution map m may contain values with the following types:
-//   * []byte - the fastest value type
-//   * string - convenient value type
-//   * TagFunc - flexible value type
+// TagFunc must write contents to w and return the number of bytes written.
+type TagFunc func(w io.Writer, tag string) (int, error)
+
+// ExecuteFunc calls f on each template tag (placeholder) occurence.
 //
 // Returns the number of bytes written to w.
-func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error) {
+func (t *Template) ExecuteFunc(w io.Writer, f TagFunc) (int64, error) {
 	var nn int64
 
 	n := len(t.texts) - 1
@@ -80,22 +88,7 @@ func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error)
 		}
 		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 {
+		if ni, err = f(w, t.tags[i]); err != nil {
 			return nn, err
 		}
 		nn += int64(ni)
@@ -108,24 +101,26 @@ func (t *Template) Execute(w io.Writer, m map[string]interface{}) (int64, error)
 	return nn, nil
 }
 
-// ExecuteString substitutes template tags (placeholders) with the corresponding
-// values from the map m and returns the result.
+// 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
 //
-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 {
+// Returns the number of bytes written to w.
+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) })
+}
+
+// ExecuteFuncString call f on each template tag (placeholder) occurence
+// and substitutes it with the data written to TagFunc's w.
+//
+// Returns the resulting string.
+func (t *Template) ExecuteFuncString(f TagFunc) string {
+	w := t.bytesBufferPool.Get().(*bytes.Buffer)
+	if _, err := t.ExecuteFunc(w, f); err != nil {
 		panic(fmt.Sprintf("unexpected error: %s", err))
 	}
 	s := string(w.Bytes())
@@ -133,3 +128,32 @@ func (t *Template) ExecuteString(m map[string]interface{}) string {
 	t.bytesBufferPool.Put(w)
 	return s
 }
+
+// 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 {
+	return t.ExecuteFuncString(func(w io.Writer, tag string) (int, error) { return stdTagFunc(w, tag, m) })
+}
+
+func stdTagFunc(w io.Writer, tag string, m map[string]interface{}) (int, error) {
+	v := m[tag]
+	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))
+	}
+}

+ 2 - 2
template_test.go

@@ -222,9 +222,9 @@ func TestMixedValues(t *testing.T) {
 	s := tpl.ExecuteString(map[string]interface{}{
 		"foo": "111",
 		"bar": []byte("bbb"),
-		"baz": TagFunc(func(w io.Writer) (int, error) { return w.Write([]byte("zzz")) }),
+		"baz": TagFunc(func(w io.Writer, tag string) (int, error) { return w.Write([]byte(tag)) }),
 	})
-	result := "foo111barbbbbazzzz"
+	result := "foo111barbbbbazbaz"
 	if s != result {
 		t.Fatalf("unexpected template value %q. Expected %q", s, result)
 	}

+ 48 - 1
timing_test.go

@@ -96,6 +96,32 @@ func BenchmarkTextTemplate(b *testing.B) {
 	})
 }
 
+func BenchmarkFastTemplateExecuteFunc(b *testing.B) {
+	t, err := NewTemplate(source, "{{", "}}")
+	if err != nil {
+		b.Fatalf("error in template: %s", err)
+	}
+
+	f := func(w io.Writer, tag string) (int, error) {
+		return w.Write(m[tag].([]byte))
+	}
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		var w bytes.Buffer
+		for pb.Next() {
+			if _, err := t.ExecuteFunc(&w, f); 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 BenchmarkFastTemplateExecute(b *testing.B) {
 	t, err := NewTemplate(source, "{{", "}}")
 	if err != nil {
@@ -118,6 +144,27 @@ func BenchmarkFastTemplateExecute(b *testing.B) {
 	})
 }
 
+func BenchmarkFastTemplateExecuteFuncString(b *testing.B) {
+	t, err := NewTemplate(source, "{{", "}}")
+	if err != nil {
+		b.Fatalf("error in template: %s", err)
+	}
+
+	f := func(w io.Writer, tag string) (int, error) {
+		return w.Write(m[tag].([]byte))
+	}
+
+	b.ResetTimer()
+	b.RunParallel(func(pb *testing.PB) {
+		for pb.Next() {
+			x := t.ExecuteFuncString(f)
+			if x != result {
+				b.Fatalf("unexpected result\n%q\nExpected\n%q\n", x, result)
+			}
+		}
+	})
+}
+
 func BenchmarkFastTemplateExecuteString(b *testing.B) {
 	t, err := NewTemplate(source, "{{", "}}")
 	if err != nil {
@@ -145,7 +192,7 @@ func BenchmarkFastTemplateExecuteTagFunc(b *testing.B) {
 	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)))) })
+			v = TagFunc(func(w io.Writer, tag string) (int, error) { return w.Write([]byte(url.QueryEscape(string(vv)))) })
 		}
 		mm[k] = v
 	}