render/extensions/extensions.go
// Copyright © 2016 Asteris, 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 extensions
import (
"bytes"
"fmt"
"sync"
"text/template"
log "github.com/Sirupsen/logrus"
"github.com/asteris-llc/converge/render/extensions/platform"
)
// RefFuncName is the name of the function to reference exported values from
// other nodes. It is a const defined here to make it easily changeable to
// avoid bikeshedding
const RefFuncName string = "lookup"
// languageKeywords defines the known keywords that have been added to the
// templating language. This is stored as a map for quick lookup and is used
// for DSL validation.
var languageKeywords = map[string]struct{}{
"env": {},
"split": {},
"join": {},
RefFuncName: {},
"platform": {},
"jsonify": {},
// functions for working with parameters
"param": {},
"paramList": {},
"paramMap": {},
}
// LanguageExtension is a type wrapper around a template.FuncMap to allow us to
// encapsulate more context in the future.
type LanguageExtension struct {
Funcs template.FuncMap
innerLock *sync.RWMutex
}
// EmptyLanguage will create an empty language
func EmptyLanguage() *LanguageExtension {
return &LanguageExtension{innerLock: new(sync.RWMutex)}
}
// MakeLanguage provides an empty language that implements a nil operation for
// all known keywords.
func MakeLanguage() *LanguageExtension {
funcs := template.FuncMap{}
for keyword := range languageKeywords {
funcs[keyword] = StubTemplateFunc
}
return &LanguageExtension{Funcs: funcs, innerLock: new(sync.RWMutex)}
}
// MinimalLanguage provides a language extension where all known extensions are
// associated with NOP functions- as with MakeLanguage()- except that arity,
// input, and output types are respected and pure transformations are
// implemented. It is less featureful than DefaultLanguage but will not
// introduce template errors that may be present when using an unmodified
// MakeLanguage.
func MinimalLanguage() *LanguageExtension {
language := MakeLanguage()
language.On("platform", newStub(&platform.Platform{}))
language.On(RefFuncName, newStub(""))
// params
language.On("param", newStub(""))
language.On("paramList", newStub([]interface{}{}))
language.On("paramMap", newStub(map[string]interface{}{}))
return language
}
// DefaultLanguage provides a default language extension. It creates default
// implementations of context-free and non-dependency-generating functions
// (e.g. split) and provides a unimplemented function for functions that must be
// supplied with context or which may register dependencies.
func DefaultLanguage() *LanguageExtension {
language := MakeLanguage()
language.On("env", DefaultEnv)
language.On("split", DefaultSplit)
language.On("join", DefaultJoin)
language.On("jsonify", DefaultJsonify)
language.On("platform", platform.DefaultPlatform)
language.On(RefFuncName, Unimplemented(RefFuncName))
// params
language.On("param", Unimplemented("param"))
language.On("paramList", Unimplemented("paramList"))
language.On("paramMap", Unimplemented("paramMap"))
language.Validate()
return language
}
// On provides a mechanism for defining an activity that will take place on
// encountering a keyword. It inserts the key and value pair into the language
// and returns a reference to the language. The language is mutated and the
// returned version is simply to allow method chaning, e.g.:
// language = MakeLanguage().On("foo", foo).On("bar", bar).On("baz", baz)
func (l *LanguageExtension) On(keyword string, action interface{}) *LanguageExtension {
l.innerLock.Lock()
defer l.innerLock.Unlock()
l.Funcs[keyword] = action
return l
}
// Join adds the keywords from toAdd that do not exist in l and adds them
func (l *LanguageExtension) Join(toAdd *LanguageExtension) *LanguageExtension {
l.innerLock.Lock()
defer l.innerLock.Unlock()
for keyword, f := range toAdd.Funcs {
if _, found := l.Funcs[keyword]; !found {
l.Funcs[keyword] = f
}
}
return l
}
// Validate checks the defined language against the known keywords and returns
// the deltas, if any. It returns true if the language exactly matches the
// known keyword list and false, with deltas, otherwise.
func (l *LanguageExtension) Validate() (missingKeywords []string, extraKeywords []string, valid bool) {
l.innerLock.Lock()
defer l.innerLock.Unlock()
var missing []string
var extra []string
ok := true
for key := range l.Funcs {
if _, found := languageKeywords[key]; !found {
extra = append(extra, key)
ok = false
}
}
for key := range languageKeywords {
if _, found := l.Funcs[key]; !found {
missing = append(missing, key)
ok = false
}
}
if !ok {
log.Printf("[WARN] bad template DSL: extra keywords: %v, missing: %v\n",
extra,
missing,
)
}
return missing, extra, ok
}
// Render provides a lightweight interface over template.New and
// template.Execute, it creates a new template given the name and input string,
// renders it with the currently defined language extensions, and writes the
// output into the provided io.Writer. If any error is returned at any point it
// is passed on to the user.
func (l *LanguageExtension) Render(dotValues interface{}, name, toRender string) (bytes.Buffer, error) {
l.innerLock.Lock()
defer l.innerLock.Unlock()
var output bytes.Buffer
tmpl, err := template.New(name).Funcs(l.Funcs).Parse(toRender)
if err != nil {
return output, err
}
err = tmpl.Execute(&output, dotValues)
return output, err
}
// StubTemplateFunc is the NOP function for template parsing
func StubTemplateFunc(...string) (string, error) {
return "", nil
}
// newStub generates a stub function that always returns returnVal when called,
// and supports a variadic number of arguments. It is used to generate stubs
// that need to return a specific value or real data type (e.g. stubs for
// `platform` which must return a valid `*platform.Platform` to prevent template
// execution errors).
func newStub(returnVal interface{}) func(...string) (interface{}, error) {
return func(...string) (interface{}, error) {
return returnVal, nil
}
}
// RememberCalls is a utility function to instert calls into a list.
// RememberCalls takes a pointer to a list of strings, and a default. It returns
// a variadic function that when called from gotemplate will take the indexed
// argument and append it to the provided list.
func RememberCalls(list *[]string, returnvalue interface{}) interface{} {
return func(params ...string) (interface{}, error) {
name := params[0]
*list = append(*list, name)
return returnvalue, nil
}
}
// Unimplemented returns a function that will raise an error with the fact that
// the keyword is unimplemented.
func Unimplemented(name string) interface{} {
return func(params ...string) (string, error) {
return "", fmt.Errorf("%s is unimplemented in the current template language", name)
}
}