171
pkg/runner/expression.go
Normal file
171
pkg/runner/expression.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/godo.v2/glob"
|
||||
)
|
||||
|
||||
const prefix = "${{"
|
||||
const suffix = "}}"
|
||||
|
||||
var pattern *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
pattern = regexp.MustCompile(fmt.Sprintf("\\%s.+?%s", prefix, suffix))
|
||||
}
|
||||
|
||||
// NewExpressionEvaluator creates a new evaluator
|
||||
func (rc *RunContext) NewExpressionEvaluator() ExpressionEvaluator {
|
||||
vm := rc.newVM()
|
||||
return &expressionEvaluator{
|
||||
vm,
|
||||
}
|
||||
}
|
||||
|
||||
// ExpressionEvaluator is the interface for evaluating expressions
|
||||
type ExpressionEvaluator interface {
|
||||
Evaluate(string) (string, error)
|
||||
Interpolate(string) (string, error)
|
||||
}
|
||||
|
||||
type expressionEvaluator struct {
|
||||
vm *otto.Otto
|
||||
}
|
||||
|
||||
func (ee *expressionEvaluator) Evaluate(in string) (string, error) {
|
||||
val, err := ee.vm.Run(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return val.ToString()
|
||||
}
|
||||
|
||||
func (ee *expressionEvaluator) Interpolate(in string) (string, error) {
|
||||
errList := make([]error, 0)
|
||||
out := pattern.ReplaceAllStringFunc(in, func(match string) string {
|
||||
expression := strings.TrimPrefix(strings.TrimSuffix(match, suffix), prefix)
|
||||
evaluated, err := ee.Evaluate(expression)
|
||||
if err != nil {
|
||||
errList = append(errList, err)
|
||||
}
|
||||
return evaluated
|
||||
})
|
||||
if len(errList) > 0 {
|
||||
return "", fmt.Errorf("Unable to interpolate string '%s' - %v", in, errList)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (rc *RunContext) newVM() *otto.Otto {
|
||||
configers := []func(*otto.Otto){
|
||||
vmContains,
|
||||
vmStartsWith,
|
||||
vmEndsWith,
|
||||
vmFormat,
|
||||
vmJoin,
|
||||
vmToJSON,
|
||||
vmHashFiles(rc.Config.Workdir),
|
||||
}
|
||||
vm := otto.New()
|
||||
for _, configer := range configers {
|
||||
configer(vm)
|
||||
}
|
||||
return vm
|
||||
}
|
||||
|
||||
func vmContains(vm *otto.Otto) {
|
||||
vm.Set("contains", func(searchString interface{}, searchValue string) bool {
|
||||
if searchStringString, ok := searchString.(string); ok {
|
||||
return strings.Contains(strings.ToLower(searchStringString), strings.ToLower(searchValue))
|
||||
} else if searchStringArray, ok := searchString.([]string); ok {
|
||||
for _, s := range searchStringArray {
|
||||
if strings.ToLower(s) == strings.ToLower(searchValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func vmStartsWith(vm *otto.Otto) {
|
||||
vm.Set("startsWith", func(searchString string, searchValue string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(searchString), strings.ToLower(searchValue))
|
||||
})
|
||||
}
|
||||
|
||||
func vmEndsWith(vm *otto.Otto) {
|
||||
vm.Set("endsWith", func(searchString string, searchValue string) bool {
|
||||
return strings.HasSuffix(strings.ToLower(searchString), strings.ToLower(searchValue))
|
||||
})
|
||||
}
|
||||
|
||||
func vmFormat(vm *otto.Otto) {
|
||||
vm.Set("format", func(s string, vals ...string) string {
|
||||
for i, v := range vals {
|
||||
s = strings.ReplaceAll(s, fmt.Sprintf("{%d}", i), v)
|
||||
}
|
||||
return s
|
||||
})
|
||||
}
|
||||
|
||||
func vmJoin(vm *otto.Otto) {
|
||||
vm.Set("join", func(element interface{}, optionalElem string) string {
|
||||
slist := make([]string, 0)
|
||||
if elementString, ok := element.(string); ok {
|
||||
slist = append(slist, elementString)
|
||||
} else if elementArray, ok := element.([]string); ok {
|
||||
slist = append(slist, elementArray...)
|
||||
}
|
||||
if optionalElem != "" {
|
||||
slist = append(slist, optionalElem)
|
||||
}
|
||||
return strings.Join(slist, " ")
|
||||
})
|
||||
}
|
||||
|
||||
func vmToJSON(vm *otto.Otto) {
|
||||
vm.Set("toJSON", func(o interface{}) string {
|
||||
rtn, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to marsal: %v", err)
|
||||
return ""
|
||||
}
|
||||
return string(rtn)
|
||||
})
|
||||
}
|
||||
|
||||
func vmHashFiles(workdir string) func(*otto.Otto) {
|
||||
return func(vm *otto.Otto) {
|
||||
vm.Set("hashFiles", func(path string) string {
|
||||
files, _, err := glob.Glob([]string{filepath.Join(workdir, path)})
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
return ""
|
||||
}
|
||||
hasher := sha256.New()
|
||||
for _, file := range files {
|
||||
f, err := os.Open(file.Path)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(hasher, f); err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
})
|
||||
}
|
||||
}
|
69
pkg/runner/expression_test.go
Normal file
69
pkg/runner/expression_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEvaluate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
rc := &RunContext{
|
||||
Config: &Config{
|
||||
Workdir: ".",
|
||||
},
|
||||
}
|
||||
ee := rc.NewExpressionEvaluator()
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
out string
|
||||
errMesg string
|
||||
}{
|
||||
{" 1 ", "1", ""},
|
||||
{"1 + 3", "4", ""},
|
||||
{"(1 + 3) * -2", "-8", ""},
|
||||
{"'my text'", "my text", ""},
|
||||
{"contains('my text', 'te')", "true", ""},
|
||||
{"contains('my TEXT', 'te')", "true", ""},
|
||||
{"contains(['my text'], 'te')", "false", ""},
|
||||
{"contains(['foo','bar'], 'bar')", "true", ""},
|
||||
{"startsWith('hello world', 'He')", "true", ""},
|
||||
{"endsWith('hello world', 'ld')", "true", ""},
|
||||
{"format('0:{0} 2:{2} 1:{1}', 'zero', 'one', 'two')", "0:zero 2:two 1:one", ""},
|
||||
{"join(['hello'],'octocat')", "hello octocat", ""},
|
||||
{"join(['hello','mona','the'],'octocat')", "hello mona the octocat", ""},
|
||||
{"join('hello','mona')", "hello mona", ""},
|
||||
{"toJSON({'foo':'bar'})", "{\n \"foo\": \"bar\"\n}", ""},
|
||||
{"hashFiles('**/package-lock.json')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
out, err := ee.Evaluate(table.in)
|
||||
if table.errMesg == "" {
|
||||
assert.NoError(err, table.in)
|
||||
assert.Equal(table.out, out)
|
||||
} else {
|
||||
assert.Error(err)
|
||||
assert.Equal(table.errMesg, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolate(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
rc := &RunContext{
|
||||
Config: &Config{
|
||||
Workdir: ".",
|
||||
},
|
||||
}
|
||||
ee := rc.NewExpressionEvaluator()
|
||||
|
||||
out, err := ee.Interpolate(" ${{1}} to ${{2}} ")
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(" 1 to 2 ", out)
|
||||
}
|
Reference in New Issue
Block a user