internal/template/template.go (126 lines of code) (raw):
// Copyright 2021 Google LLC.
//
// 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 provides utility functions around reading and writing templates.
package template
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/google/go-cpy/cpy"
"github.com/imdario/mergo"
)
var mergoOpts = []func(*mergo.Config){
mergo.WithOverride,
mergo.WithOverwriteWithEmptyValue,
}
var copier = cpy.New(
cpy.IgnoreAllUnexported(),
)
// FlattenInfo describes keys to flatten. If index is not a nil pointer then it is assumed the
// value is a list that must be flattened at the specific index.
type FlattenInfo struct {
Key string `hcl:"key,attr" json:"key"`
Index *int `hcl:"index,optional" json:"index,omitempty"`
}
// WriteDir generates files to directory `outputDir` based on templates from directory `inputDir` and `data`.
func WriteDir(inputDir, outputDir string, data map[string]interface{}) error {
fs, err := ioutil.ReadDir(inputDir)
if err != nil {
return fmt.Errorf("read dir %q: %v", inputDir, err)
}
if len(fs) == 0 {
// Just create the output directory with no files.
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("create dir %q: %v", outputDir, err)
}
return nil
}
for _, f := range fs {
in := filepath.Join(inputDir, f.Name())
out := filepath.Join(outputDir, f.Name())
if f.IsDir() {
if err := WriteDir(in, out, data); err != nil {
return err
}
continue
}
if err := WriteFile(in, out, data); err != nil {
return err
}
}
return nil
}
// Any files with this extension will have it removed before being written out.
const tmplExt = ".tmpl"
// WriteFile generates `out` based on the `in` template and `data`.
func WriteFile(in, out string, data map[string]interface{}) error {
if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil {
return fmt.Errorf("mkdir %q: %v", filepath.Dir(out), err)
}
b, err := ioutil.ReadFile(in)
if err != nil {
return fmt.Errorf("read %q: %v", in, err)
}
tmpl, err := template.New(in).Funcs(funcMap).Option("missingkey=error").Parse(string(b))
if err != nil {
return fmt.Errorf("parse template %q: %v", in, err)
}
// Remove the template extension if present
out = strings.TrimSuffix(out, tmplExt)
outFile, err := os.OpenFile(out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer outFile.Close()
if err := tmpl.Execute(outFile, data); err != nil {
return fmt.Errorf("execute template %q: %v", in, err)
}
return nil
}
// WriteBuffer creates a buffer with template `text` filled with values from `data`.
func WriteBuffer(text string, data map[string]interface{}) (*bytes.Buffer, error) {
var buf bytes.Buffer
tmpl, err := template.New("").Funcs(funcMap).Option("missingkey=error").Parse(text)
if err != nil {
return nil, err
}
if err := tmpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("execute buffer %s with data %v: %v", text, data, err)
}
return &buf, nil
}
// MergeData merges template data from src to dst.
// For all keys in flatten it will pop the key and merge back into dst.
func MergeData(dst map[string]interface{}, src map[string]interface{}) error {
if dst == nil {
return errors.New("dst must not be nil")
}
return mergo.Merge(&dst, src, mergoOpts...)
}
// CopyData creates a deep copy of the argument map src.
func CopyData(src map[string]interface{}) (map[string]interface{}, error) {
data, ok := copier.Copy(src).(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to assert output data type during data copy")
}
return data, nil
}
// FlattenData returns the map of kes from src flattened into a single map.
func FlattenData(src map[string]interface{}, fis []*FlattenInfo) (map[string]interface{}, error) {
res := make(map[string]interface{})
for _, fi := range fis {
v := get(src, fi.Key)
if v == nil {
v = get(res, fi.Key) // This allows you to flatten inner keys
if v == nil {
return nil, fmt.Errorf("flatten key %q not found in data: %v", fi.Key, src)
}
}
delete(src, fi.Key)
// If index is set assume value is a list and the index is being flattened.
if i := fi.Index; i != nil {
vs := v.([]interface{})
if *i >= len(vs) {
return nil, fmt.Errorf("flatten index for key %q out of range: got %v, want value between 0 and %v", fi.Key, fi.Index, len(vs))
}
v = vs[*i]
}
m, ok := v.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("flatten key %q is not a map, got type %T, value %v", fi.Key, v, v)
}
if err := MergeData(res, m); err != nil {
return nil, err
}
}
return res, nil
}