Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support custom celEnvs when running expressions #46

Merged
merged 2 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions structtemplater.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type StructTemplater struct {
Values map[string]interface{}
// IgnoreFields from walking where key is field name and value is field type
IgnoreFields map[string]string
Funcs map[string]func() any
Funcs map[string]any
DelimSets []Delims
// If specified create a function for each value so that is can be accessed via {{ value }} in addition to {{ .value }}
ValueFunctions bool
Expand Down Expand Up @@ -97,7 +97,7 @@ func (w StructTemplater) Template(val string) (string, error) {
return val, nil
}
if w.Funcs == nil {
w.Funcs = make(map[string]func() any)
w.Funcs = make(map[string]any)
}
if w.ValueFunctions {
for k, v := range w.Values {
Expand Down
86 changes: 57 additions & 29 deletions template.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"fmt"
"os"
"reflect"
"sort"
"strings"
gotemplate "text/template"
Expand Down Expand Up @@ -35,13 +34,23 @@ func init() {
}

type Template struct {
Template string `yaml:"template,omitempty" json:"template,omitempty"` // Go template
JSONPath string `yaml:"jsonPath,omitempty" json:"jsonPath,omitempty"`
Expression string `yaml:"expr,omitempty" json:"expr,omitempty"` // A cel-go expression
Javascript string `yaml:"javascript,omitempty" json:"javascript,omitempty"`
Functions map[string]func() any `yaml:"-" json:"-"`
RightDelim string `yaml:"-" json:"-"`
LeftDelim string `yaml:"-" json:"-"`
Template string `yaml:"template,omitempty" json:"template,omitempty"` // Go template
JSONPath string `yaml:"jsonPath,omitempty" json:"jsonPath,omitempty"`
Expression string `yaml:"expr,omitempty" json:"expr,omitempty"` // A cel-go expression
Javascript string `yaml:"javascript,omitempty" json:"javascript,omitempty"`
RightDelim string `yaml:"-" json:"-"`
LeftDelim string `yaml:"-" json:"-"`

// Pass in additional cel-env options like functions
// that aren't simple enough to be included in Functions
CelEnvs []cel.EnvOption `yaml:"-" json:"-"`

// A map of functions that are accessible to cel expressions
// and go templates.
// NOTE: For cel expressions, the functions must be of type func() any.
// If any other function type is used, an error will be returned.
// Opt to CelEnvs for those cases.
Functions map[string]any `yaml:"-" json:"-"`
}

func (t Template) CacheKey(env map[string]any) string {
Expand All @@ -51,18 +60,26 @@ func (t Template) CacheKey(env map[string]any) string {
}
sort.Slice(envVars, func(i, j int) bool { return envVars[i] < envVars[j] })

funcNames := make([]string, 0, len(t.Functions))
for k := range t.Functions {
funcNames = append(funcNames, k)
}
sort.Slice(funcNames, func(i, j int) bool { return funcNames[i] < funcNames[j] })

funcKeys := make([]string, 0, len(t.Functions))
for _, fnName := range funcNames {
funcKeys = append(funcKeys, fmt.Sprintf("%d", reflect.ValueOf(t.Functions[fnName]).Pointer()))
}
return strings.Join(envVars, "-") +
t.RightDelim +
t.LeftDelim +
t.Expression +
t.Javascript +
t.JSONPath +
t.Template
}

return strings.Join(envVars, "-") + strings.Join(funcKeys, "-") + t.RightDelim + t.LeftDelim + t.Expression + t.Javascript + t.JSONPath + t.Template
func (t Template) IsCacheable() bool {
// Note: If custom functions are provided then we don't cache the template
// because it's not possible to uniquely identify a function to be used as a cache key.
// Pointers don't work well because different functions, that are behaviourly different,
// but syntatically identical, will have the same pointer value.
//
// Reference: https://pkg.go.dev/reflect#Value.Pointer
// > If v's Kind is Func, the returned pointer is an underlying code pointer,
// > but not necessarily enough to identify a single function uniquely.
// > The only guarantee is that the result is zero if and only if v is a nil func Value.
return len(t.CelEnvs) == 0 && len(t.Functions) == 0
}

func (t Template) IsEmpty() bool {
Expand All @@ -84,17 +101,26 @@ func RunExpression(_environment map[string]any, template Template) (any, error)
nil,
cel.AnyType,
cel.FunctionBinding(func(values ...ref.Val) ref.Val {
out := _fn()
ogFunc, ok := _fn.(func() any)
if !ok {
return types.WrapErr(fmt.Errorf("%s is expected to be of type func() any", _name))
}

out := ogFunc()
return types.DefaultTypeAdapter.NativeToValue(out)
}),
)))
}

envOptions = append(envOptions, template.CelEnvs...)

var prg cel.Program
cached, ok := celExpressionCache.Get(template.CacheKey(_environment))
if ok {
if cachedPrg, ok := cached.(*cel.Program); ok {
prg = *cachedPrg
if template.IsCacheable() {
cached, ok := celExpressionCache.Get(template.CacheKey(_environment))
if ok {
if cachedPrg, ok := cached.(*cel.Program); ok {
prg = *cachedPrg
}
}
}

Expand Down Expand Up @@ -166,10 +192,13 @@ func RunTemplate(environment map[string]any, template Template) (string, error)

func goTemplate(template Template, environment map[string]any) (string, error) {
var tpl *gotemplate.Template
cached, ok := goTemplateCache.Get(template.CacheKey(nil))
if ok {
if cachedTpl, ok := cached.(*gotemplate.Template); ok {
tpl = cachedTpl

if template.IsCacheable() {
cached, ok := goTemplateCache.Get(template.CacheKey(nil))
if ok {
if cachedTpl, ok := cached.(*gotemplate.Template); ok {
tpl = cachedTpl
}
}
}

Expand All @@ -186,7 +215,6 @@ func goTemplate(template Template, environment map[string]any) (string, error) {
for k, v := range template.Functions {
funcs[k] = v
}

var err error
tpl, err = tpl.Funcs(funcs).Parse(template.Template)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestCacheKeyConsistency(t *testing.T) {
{
tt := Template{
Expression: "{{.name}}{{.age}}",
Functions: map[string]func() any{
Functions: map[string]any{
"hello": hello,
"Hello": foo,
"foo": foo,
Expand All @@ -41,7 +41,7 @@ func TestCacheKeyConsistency(t *testing.T) {
Template: "{{.name}}{{.age}}",
LeftDelim: "{{",
RightDelim: "}}",
Functions: map[string]func() any{
Functions: map[string]any{
"hello": hello,
"Hello": foo,
"foo": foo,
Expand Down
2 changes: 1 addition & 1 deletion tests/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func runTests(t *testing.T, tests []Test) {
}

func TestFunctions(t *testing.T) {
funcs := map[string]func() any{
funcs := map[string]any{
"fn": func() any {
return map[string]any{
"a": "b",
Expand Down
2 changes: 1 addition & 1 deletion tests/gomplate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

func TestGomplateFunctions(t *testing.T) {
funcs := map[string]func() any{
funcs := map[string]any{
"fn": func() any {
return map[string]any{
"a": "b",
Expand Down
Loading