asteris-llc/converge

View on GitHub
render/extensions/extensions.go

Summary

Maintainability
A
0 mins
Test Coverage
// 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)
    }
}