tools/src: Move template logic of 'gen' to package
And add tests.
This is useful for other tooling.
Change-Id: Ia399071baf6d4bb617f3c73e4ccd4ed72d522c2e
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/111020
Commit-Queue: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
diff --git a/tools/src/cmd/gen/main.go b/tools/src/cmd/gen/main.go
index 653adbb..cdf91da 100644
--- a/tools/src/cmd/gen/main.go
+++ b/tools/src/cmd/gen/main.go
@@ -30,13 +30,12 @@
"runtime"
"strconv"
"strings"
- "text/template"
"time"
- "unicode"
"dawn.googlesource.com/dawn/tools/src/container"
"dawn.googlesource.com/dawn/tools/src/fileutils"
"dawn.googlesource.com/dawn/tools/src/glob"
+ "dawn.googlesource.com/dawn/tools/src/template"
"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/gen"
"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/parser"
"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/resolver"
@@ -325,7 +324,6 @@
`
type generator struct {
- template *template.Template
cache *genCache
writeFile WriteFile
rnd *rand.Rand
@@ -342,32 +340,13 @@
// syntax.
func generate(tmpl string, cache *genCache, w io.Writer, writeFile WriteFile) error {
g := generator{
- template: template.New("<template>"),
cache: cache,
writeFile: writeFile,
rnd: rand.New(rand.NewSource(4561123)),
}
- if err := g.bindAndParse(g.template, tmpl); err != nil {
- return err
- }
- return g.template.Execute(w, nil)
-}
-func (g *generator) bindAndParse(t *template.Template, text string) error {
- _, err := t.Funcs(map[string]interface{}{
- "Map": newMap,
- "Iterate": iterate,
- "Title": strings.Title,
- "PascalCase": pascalCase,
+ funcs := map[string]interface{}{
"SplitDisplayName": gen.SplitDisplayName,
- "Contains": strings.Contains,
- "HasPrefix": strings.HasPrefix,
- "HasSuffix": strings.HasSuffix,
- "TrimPrefix": strings.TrimPrefix,
- "TrimSuffix": strings.TrimSuffix,
- "TrimLeft": strings.TrimLeft,
- "TrimRight": strings.TrimRight,
- "Split": strings.Split,
"Scramble": g.scramble,
"IsEnumEntry": is(sem.EnumEntry{}),
"IsEnumMatcher": is(sem.EnumMatcher{}),
@@ -387,63 +366,9 @@
"Sem": g.cache.sem,
"IntrinsicTable": g.cache.intrinsicTable,
"Permute": g.cache.permute,
- "Eval": g.eval,
- "Import": g.importTmpl,
"WriteFile": func(relPath, content string) (string, error) { return "", g.writeFile(relPath, content) },
- }).Option("missingkey=error").Parse(text)
- return err
-}
-
-// eval executes the sub-template with the given name and argument, returning
-// the generated output
-func (g *generator) eval(template string, args ...interface{}) (string, error) {
- target := g.template.Lookup(template)
- if target == nil {
- return "", fmt.Errorf("template '%v' not found", template)
}
- sb := strings.Builder{}
-
- var err error
- if len(args) == 1 {
- err = target.Execute(&sb, args[0])
- } else {
- m := newMap()
- if len(args)%2 != 0 {
- return "", fmt.Errorf("Eval expects a single argument or list name-value pairs")
- }
- for i := 0; i < len(args); i += 2 {
- name, ok := args[i].(string)
- if !ok {
- return "", fmt.Errorf("Eval argument %v is not a string", i)
- }
- m.Put(name, args[i+1])
- }
- err = target.Execute(&sb, m)
- }
-
- if err != nil {
- return "", fmt.Errorf("while evaluating '%v': %v", template, err)
- }
- return sb.String(), nil
-}
-
-// importTmpl parses the template at the given project-relative path, merging
-// the template definitions into the global namespace.
-// Note: The body of the template is not executed.
-func (g *generator) importTmpl(path string) (string, error) {
- if strings.Contains(path, "..") {
- return "", fmt.Errorf("import path must not contain '..'")
- }
- path = filepath.Join(fileutils.DawnRoot(), path)
- data, err := ioutil.ReadFile(path)
- if err != nil {
- return "", fmt.Errorf("failed to open '%v': %w", path, err)
- }
- t := g.template.New("")
- if err := g.bindAndParse(t, string(data)); err != nil {
- return "", fmt.Errorf("failed to parse '%v': %w", path, err)
- }
- return "", nil
+ return template.Run(tmpl, w, funcs)
}
// scramble randomly modifies the input string so that it is no longer equal to
@@ -477,24 +402,6 @@
return string(bytes), nil
}
-// Map is a simple generic key-value map, which can be used in the template
-type Map map[interface{}]interface{}
-
-func newMap() Map { return Map{} }
-
-// Put adds the key-value pair into the map.
-// Put always returns an empty string so nothing is printed in the template.
-func (m Map) Put(key, value interface{}) string {
- m[key] = value
- return ""
-}
-
-// Get looks up and returns the value with the given key. If the map does not
-// contain the given key, then nil is returned.
-func (m Map) Get(key interface{}) interface{} {
- return m[key]
-}
-
// is returns a function that returns true if the value passed to the function
// matches the type of 'ty'.
func is(ty interface{}) func(interface{}) bool {
@@ -525,43 +432,6 @@
return s.Index(count-1).Interface() == v
}
-// iterate returns a slice of length 'n', with each element equal to its index.
-// Useful for: {{- range Iterate $n -}}<this will be looped $n times>{{end}}
-func iterate(n int) []int {
- out := make([]int, n)
- for i := range out {
- out[i] = i
- }
- return out
-}
-
-// pascalCase returns the snake-case string s transformed into 'PascalCase',
-// Rules:
-// * The first letter of the string is capitalized
-// * Characters following an underscore or number are capitalized
-// * Underscores are removed from the returned string
-// See: https://en.wikipedia.org/wiki/Camel_case
-func pascalCase(s string) string {
- b := strings.Builder{}
- upper := true
- for _, r := range s {
- if r == '_' {
- upper = true
- continue
- }
- if upper {
- b.WriteRune(unicode.ToUpper(r))
- upper = false
- } else {
- b.WriteRune(r)
- }
- if unicode.IsNumber(r) {
- upper = true
- }
- }
- return b.String()
-}
-
// Invokes the clang-format executable at 'exe' to format the file content 'in'.
// Returns the formatted file.
func clangFormat(in, exe string) (string, error) {
diff --git a/tools/src/template/template.go b/tools/src/template/template.go
new file mode 100644
index 0000000..d70635b
--- /dev/null
+++ b/tools/src/template/template.go
@@ -0,0 +1,190 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// package template wraps the golang "text/template" package to provide an
+// enhanced template generator.
+package template
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "path/filepath"
+ "strings"
+ "text/template"
+ "unicode"
+
+ "dawn.googlesource.com/dawn/tools/src/fileutils"
+)
+
+// The template function binding table
+type Functions map[string]interface{}
+
+// Run executes the template tmpl, writing the output to w.
+// funcs are the functions provided to the template.
+// See https://golang.org/pkg/text/template/ for documentation on the template
+// syntax.
+func Run(tmpl string, w io.Writer, funcs Functions) error {
+ g := generator{
+ template: template.New("<template>"),
+ }
+
+ // Add a bunch of generic useful functions
+ g.funcs = Functions{
+ "Contains": strings.Contains,
+ "Eval": g.eval,
+ "HasPrefix": strings.HasPrefix,
+ "HasSuffix": strings.HasSuffix,
+ "Import": g.importTmpl,
+ "Iterate": iterate,
+ "Map": newMap,
+ "PascalCase": pascalCase,
+ "Split": strings.Split,
+ "Title": strings.Title,
+ "TrimLeft": strings.TrimLeft,
+ "TrimPrefix": strings.TrimPrefix,
+ "TrimRight": strings.TrimRight,
+ "TrimSuffix": strings.TrimSuffix,
+ }
+
+ // Append custom functions
+ for name, fn := range funcs {
+ g.funcs[name] = fn
+ }
+
+ if err := g.bindAndParse(g.template, tmpl); err != nil {
+ return err
+ }
+
+ return g.template.Execute(w, nil)
+}
+
+type generator struct {
+ template *template.Template
+ funcs Functions
+}
+
+func (g *generator) bindAndParse(t *template.Template, tmpl string) error {
+ _, err := t.
+ Funcs(map[string]interface{}(g.funcs)).
+ Option("missingkey=error").
+ Parse(tmpl)
+ return err
+}
+
+// eval executes the sub-template with the given name and argument, returning
+// the generated output
+func (g *generator) eval(template string, args ...interface{}) (string, error) {
+ target := g.template.Lookup(template)
+ if target == nil {
+ return "", fmt.Errorf("template '%v' not found", template)
+ }
+ sb := strings.Builder{}
+
+ var err error
+ if len(args) == 1 {
+ err = target.Execute(&sb, args[0])
+ } else {
+ m := newMap()
+ if len(args)%2 != 0 {
+ return "", fmt.Errorf("Eval expects a single argument or list name-value pairs")
+ }
+ for i := 0; i < len(args); i += 2 {
+ name, ok := args[i].(string)
+ if !ok {
+ return "", fmt.Errorf("Eval argument %v is not a string", i)
+ }
+ m.Put(name, args[i+1])
+ }
+ err = target.Execute(&sb, m)
+ }
+
+ if err != nil {
+ return "", fmt.Errorf("while evaluating '%v': %v", template, err)
+ }
+ return sb.String(), nil
+}
+
+// importTmpl parses the template at the given project-relative path, merging
+// the template definitions into the global namespace.
+// Note: The body of the template is not executed.
+func (g *generator) importTmpl(path string) (string, error) {
+ if strings.Contains(path, "..") {
+ return "", fmt.Errorf("import path must not contain '..'")
+ }
+ path = filepath.Join(fileutils.DawnRoot(), path)
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ return "", fmt.Errorf("failed to open '%v': %w", path, err)
+ }
+ if err := g.bindAndParse(g.template.New(""), string(data)); err != nil {
+ return "", fmt.Errorf("failed to parse '%v': %w", path, err)
+ }
+ return "", nil
+}
+
+// Map is a simple generic key-value map, which can be used in the template
+type Map map[interface{}]interface{}
+
+func newMap() Map { return Map{} }
+
+// Put adds the key-value pair into the map.
+// Put always returns an empty string so nothing is printed in the template.
+func (m Map) Put(key, value interface{}) string {
+ m[key] = value
+ return ""
+}
+
+// Get looks up and returns the value with the given key. If the map does not
+// contain the given key, then nil is returned.
+func (m Map) Get(key interface{}) interface{} {
+ return m[key]
+}
+
+// iterate returns a slice of length 'n', with each element equal to its index.
+// Useful for: {{- range Iterate $n -}}<this will be looped $n times>{{end}}
+func iterate(n int) []int {
+ out := make([]int, n)
+ for i := range out {
+ out[i] = i
+ }
+ return out
+}
+
+// pascalCase returns the snake-case string s transformed into 'PascalCase',
+// Rules:
+// * The first letter of the string is capitalized
+// * Characters following an underscore or number are capitalized
+// * Underscores are removed from the returned string
+// See: https://en.wikipedia.org/wiki/Camel_case
+func pascalCase(s string) string {
+ b := strings.Builder{}
+ upper := true
+ for _, r := range s {
+ if r == '_' || r == ' ' {
+ upper = true
+ continue
+ }
+ if upper {
+ b.WriteRune(unicode.ToUpper(r))
+ upper = false
+ } else {
+ b.WriteRune(r)
+ }
+ if unicode.IsNumber(r) {
+ upper = true
+ }
+ }
+ return b.String()
+}
diff --git a/tools/src/template/template_test.go b/tools/src/template/template_test.go
new file mode 100644
index 0000000..f18e727
--- /dev/null
+++ b/tools/src/template/template_test.go
@@ -0,0 +1,225 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package template_test
+
+import (
+ "bytes"
+ "testing"
+
+ "dawn.googlesource.com/dawn/tools/src/template"
+ "github.com/google/go-cmp/cmp"
+)
+
+func check(t *testing.T, tmpl, expected string, fns template.Functions) {
+ t.Helper()
+ w := &bytes.Buffer{}
+ err := template.Run(tmpl, w, fns)
+ if err != nil {
+ t.Errorf("template.Run() failed with %v", err)
+ return
+ }
+ got := w.String()
+ if diff := cmp.Diff(expected, got); diff != "" {
+ t.Errorf("output was not as expected. Diff:\n%v", diff)
+ }
+}
+
+func TestContains(t *testing.T) {
+ tmpl := `
+{{ Contains "hello world" "hello"}}
+{{ Contains "hello world" "fish"}}
+`
+ expected := `
+true
+false
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestEvalSingleParameter(t *testing.T) {
+ tmpl := `
+pre-eval
+{{ Eval "T" 123 }}
+{{ Eval "T" "cat" }}
+post-eval
+
+pre-define
+{{- define "T"}}
+ . is {{.}}
+{{- end }}
+post-define
+`
+ expected := `
+pre-eval
+
+ . is 123
+
+ . is cat
+post-eval
+
+pre-define
+post-define
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestEvalParameterPairs(t *testing.T) {
+ tmpl := `
+pre-eval
+{{ Eval "T" "number" 123 "animal" "cat" }}
+post-eval
+
+pre-define
+{{- define "T"}}
+ .number is {{.number}}
+ .animal is {{.animal}}
+{{- end }}
+post-define
+`
+ expected := `
+pre-eval
+
+ .number is 123
+ .animal is cat
+post-eval
+
+pre-define
+post-define
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestHasPrefix(t *testing.T) {
+ tmpl := `
+{{ HasPrefix "hello world" "hello"}}
+{{ HasPrefix "hello world" "world"}}
+`
+ expected := `
+true
+false
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestIterate(t *testing.T) {
+ tmpl := `
+{{- range $i := Iterate 5}}
+ {{$i}}
+{{- end}}
+`
+ expected := `
+ 0
+ 1
+ 2
+ 3
+ 4
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestMap(t *testing.T) {
+ tmpl := `
+ {{- $m := Map }}
+ {{- $m.Put "one" 1 }}
+ {{- $m.Put "two" 2 }}
+ one: {{ $m.Get "one" }}
+ two: {{ $m.Get "two" }}
+`
+ expected := `
+ one: 1
+ two: 2
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestPascalCase(t *testing.T) {
+ tmpl := `
+{{ PascalCase "hello world" }}
+{{ PascalCase "hello_world" }}
+`
+ expected := `
+HelloWorld
+HelloWorld
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestSplit(t *testing.T) {
+ tmpl := `
+{{- range $i, $s := Split "cat_says_meow" "_" }}
+ {{$i}}: '{{$s}}'
+{{- end }}
+`
+ expected := `
+ 0: 'cat'
+ 1: 'says'
+ 2: 'meow'
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TestTitle(t *testing.T) {
+ tmpl := `
+{{Title "hello world"}}
+`
+ expected := `
+Hello World
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TrimLeft(t *testing.T) {
+ tmpl := `
+'{{TrimLeft "hello world", "hel"}}'
+`
+ expected := `
+'o world'
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TrimPrefix(t *testing.T) {
+ tmpl := `
+'{{TrimLeft "hello world", "hel"}}'
+'{{TrimLeft "hello world", "heo"}}'
+`
+ expected := `
+'o world'
+'hello world'
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TrimRight(t *testing.T) {
+ tmpl := `
+'{{TrimRight "hello world", "wld"}}'
+`
+ expected := `
+'hello wor'
+`
+ check(t, tmpl, expected, nil)
+}
+
+func TrimSuffix(t *testing.T) {
+ tmpl := `
+'{{TrimRight "hello world", "rld"}}'
+'{{TrimRight "hello world", "wld"}}'
+`
+ expected := `
+'hello wo'
+'hello world'
+`
+ check(t, tmpl, expected, nil)
+}