diff --git a/.gitignore b/.gitignore index 5e3002f..ecca56a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +.env +.vscode/ + vendor/ /.glide diff --git a/alias.go b/alias.go new file mode 100644 index 0000000..2e5ef15 --- /dev/null +++ b/alias.go @@ -0,0 +1,72 @@ +package sprout + +// BACKWARDS COMPATIBILITY +// The following functions are provided for backwards compatibility with the +// original sprig methods. They are not recommended for use in new code. +var bc_registerSprigFuncs = map[string][]string{ + "dateModify": {"date_modify"}, //! Deprecated: Should use dateModify instead + "dateInZone": {"date_in_zone"}, //! Deprecated: Should use dateInZone instead + "mustDateModify": {"must_date_modify"}, //! Deprecated: Should use mustDateModify instead + "ellipsis": {"abbrev"}, //! Deprecated: Should use ellipsis instead + "ellipsisBoth": {"abbrevboth"}, //! Deprecated: Should use ellipsisBoth instead + "trimAll": {"trimall"}, //! Deprecated: Should use trimAll instead + "int": {"atoi"}, //! Deprecated: Should use toInt instead + "append": {"push"}, //! Deprecated: Should use append instead + "mustAppend": {"mustPush"}, //! Deprecated: Should use mustAppend instead + "list": {"tuple"}, // FIXME: with the addition of append/prepend these are no longer immutable. + "max": {"biggest"}, +} + +//\ BACKWARDS COMPATIBILITY + +// WithAlias returns a FunctionHandlerOption that associates one or more alias +// names with an original function name. +// This allows the function to be called by any of its aliases. +// +// originalFunction specifies the original function name to which aliases will +// be added. aliases is a variadic parameter that takes one or more strings as +// aliases for the original function. +// +// The function does nothing if no aliases are provided. +// If the original function name does not already have associated aliases in +// the FunctionHandler, a new slice of strings is created to hold its aliases. +// Each provided alias is then appended to this slice. +// +// This option must be applied to a FunctionHandler using the FunctionHandler's +// options mechanism for the aliases to take effect. +func WithAlias(originalFunction string, aliases ...string) FunctionHandlerOption { + return func(p *FunctionHandler) { + if len(aliases) == 0 { + return + } + + if _, ok := p.funcsAlias[originalFunction]; !ok { + p.funcsAlias[originalFunction] = make([]string, 0) + } + + p.funcsAlias[originalFunction] = append(p.funcsAlias[originalFunction], aliases...) + } +} + +// registerAliases allows the aliases to be used as references to the original +// functions. +// +// It should be called after all aliases have been added through the WithAlias +// option and before the function map is used to ensure all aliases are properly +// registered. +func (p *FunctionHandler) registerAliases() { + // BACKWARDS COMPATIBILITY + // Register the sprig function aliases + for originalFunction, aliases := range bc_registerSprigFuncs { + for _, alias := range aliases { + p.funcMap[alias] = p.funcMap[originalFunction] + } + } + //\ BACKWARDS COMPATIBILITY + + for originalFunction, aliases := range p.funcsAlias { + for _, alias := range aliases { + p.funcMap[alias] = p.funcMap[originalFunction] + } + } +} diff --git a/alias_test.go b/alias_test.go new file mode 100644 index 0000000..82a0678 --- /dev/null +++ b/alias_test.go @@ -0,0 +1,75 @@ +package sprout + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestWithAlias checks that aliases are correctly added to a function. +func TestWithAlias(t *testing.T) { + handler := NewFunctionHandler() + originalFunc := "originalFunc" + alias1 := "alias1" + alias2 := "alias2" + + // Apply the WithAlias option with two aliases. + WithAlias(originalFunc, alias1, alias2)(handler) + + // Check that the aliases were added. + assert.Contains(t, handler.funcsAlias, originalFunc) + assert.Contains(t, handler.funcsAlias[originalFunc], alias1) + assert.Contains(t, handler.funcsAlias[originalFunc], alias2) + assert.Len(t, handler.funcsAlias[originalFunc], 2, "there should be exactly 2 aliases") +} + +func TestWithAlias_Empty(t *testing.T) { + handler := NewFunctionHandler() + originalFunc := "originalFunc" + + // Apply the WithAlias option with no aliases. + WithAlias(originalFunc)(handler) + + // Check that no aliases were added. + assert.NotContains(t, handler.funcsAlias, originalFunc) +} + +// TestRegisterAliases checks that aliases are correctly registered in the function map. +func TestRegisterAliases(t *testing.T) { + handler := NewFunctionHandler() + originalFunc := "originalFunc" + alias1 := "alias1" + alias2 := "alias2" + + // Mock a function for originalFunc and add it to funcMap. + mockFunc := func() {} + handler.funcMap[originalFunc] = mockFunc + + // Apply the WithAlias option and then register the aliases. + WithAlias(originalFunc, alias1, alias2)(handler) + handler.registerAliases() + + // Check that the aliases are mapped to the same function as the original function in funcMap. + assert.True(t, reflect.ValueOf(handler.funcMap[originalFunc]).Pointer() == reflect.ValueOf(handler.funcMap[alias1]).Pointer()) + assert.True(t, reflect.ValueOf(handler.funcMap[originalFunc]).Pointer() == reflect.ValueOf(handler.funcMap[alias2]).Pointer()) +} + +func TestAliasesInTemplate(t *testing.T) { + handler := NewFunctionHandler() + originalFuncName := "originalFunc" + alias1 := "alias1" + alias2 := "alias2" + + // Mock a function for originalFunc and add it to funcMap. + mockFunc := func() string { return "cheese" } + handler.funcMap[originalFuncName] = mockFunc + + // Apply the WithAlias option and then register the aliases. + WithAlias(originalFuncName, alias1, alias2)(handler) + + // Create a template with the aliases. + result, err := runTemplate(t, handler, `{{originalFunc}} {{alias1}} {{alias2}}`) + assert.NoError(t, err) + assert.Equal(t, "cheese cheese cheese", result) +} diff --git a/functions.go b/functions.go index e624880..46183c7 100644 --- a/functions.go +++ b/functions.go @@ -8,7 +8,6 @@ import ( "path" "path/filepath" "reflect" - "strconv" "strings" ttemplate "text/template" "time" @@ -50,32 +49,22 @@ var genericMap = map[string]interface{}{ "hello": func() string { return "Hello!" }, // Date functions - "ago": dateAgo, - "date": date, - //! Deprecated: Should use dateModify instead - "date_modify": dateModify, - "dateModify": dateModify, - //! Deprecated: Should use dateInZone instead - "date_in_zone": dateInZone, + "ago": dateAgo, + "date": date, + "dateModify": dateModify, "dateInZone": dateInZone, "duration": duration, "durationRound": durationRound, "htmlDate": htmlDate, "htmlDateInZone": htmlDateInZone, - //! Deprecated: Should use mustDateModify instead - "must_date_modify": mustDateModify, - "mustDateModify": mustDateModify, - "mustToDate": mustToDate, - "now": time.Now, - "toDate": toDate, - "unixEpoch": unixEpoch, + "mustDateModify": mustDateModify, + "mustToDate": mustToDate, + "now": time.Now, + "toDate": toDate, + "unixEpoch": unixEpoch, // Strings - //! Deprecated: Should use ellipsis instead - "abbrev": func(width int, str string) string { return ellipsis(str, 0, width) }, - "ellipsis": func(width int, str string) string { return ellipsis(str, 0, width) }, - //! Deprecated: Should use ellipsisBoth instead - "abbrevboth": func(left, right int, str string) string { return ellipsis(str, left, right) }, + "ellipsis": func(width int, str string) string { return ellipsis(str, 0, width) }, "ellipsisBoth": func(left, right int, str string) string { return ellipsis(str, left, right) }, "trunc": trunc, "trim": strings.TrimSpace, @@ -86,8 +75,6 @@ var genericMap = map[string]interface{}{ "substr": substring, // Switch order so that "foo" | repeat 5 "repeat": func(count int, str string) string { return strings.Repeat(str, count) }, - // Deprecated: Use trimAll. - "trimall": func(a, b string) string { return strings.Trim(b, a) }, // Switch order so that "$foo" | trimall "$" "trimAll": func(a, b string) string { return strings.Trim(b, a) }, "trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) }, @@ -122,7 +109,6 @@ var genericMap = map[string]interface{}{ "toString": strval, // Wrap Atoi to stop errors. - "atoi": func(a string) int { i, _ := strconv.Atoi(a); return i }, "int64": toInt64, "int": toInt, "float64": toFloat64, @@ -180,14 +166,13 @@ var genericMap = map[string]interface{}{ "mulf": func(a interface{}, v ...interface{}) float64 { return execDecimalOp(a, v, func(d1, d2 decimal.Decimal) decimal.Decimal { return d1.Mul(d2) }) }, - "biggest": max, - "max": max, - "min": min, - "maxf": maxf, - "minf": minf, - "ceil": ceil, - "floor": floor, - "round": round, + "max": max, + "min": min, + "maxf": maxf, + "minf": minf, + "ceil": ceil, + "floor": floor, + "round": round, // string slices. Note that we reverse the order b/c that's better // for template processing. @@ -250,7 +235,6 @@ var genericMap = map[string]interface{}{ "b32dec": base32decode, // Data Structures: - "tuple": list, // FIXME: with the addition of append/prepend these are no longer immutable. "list": list, "dict": dict, "get": get, @@ -267,8 +251,8 @@ var genericMap = map[string]interface{}{ "mustMergeOverwrite": mustMergeOverwrite, "values": values, - "append": push, "push": push, - "mustAppend": mustPush, "mustPush": mustPush, + "append": push, + "mustAppend": mustPush, "prepend": prepend, "mustPrepend": mustPrepend, "first": first, diff --git a/functions_test.go b/functions_test.go index 30e8276..6dbd82d 100644 --- a/functions_test.go +++ b/functions_test.go @@ -104,8 +104,7 @@ func runt(tpl, expect string) error { // // It runs the template and verifies that the output is an exact match. func runtv(tpl, expect string, vars interface{}) error { - fmap := TxtFuncMap() - t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + t := template.Must(template.New("test").Funcs(FuncMap()).Parse(tpl)) var b bytes.Buffer err := t.Execute(&b, vars) if err != nil { @@ -119,8 +118,7 @@ func runtv(tpl, expect string, vars interface{}) error { // runRaw runs a template with the given variables and returns the result. func runRaw(tpl string, vars interface{}) (string, error) { - fmap := TxtFuncMap() - t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + t := template.Must(template.New("test").Funcs(FuncMap()).Parse(tpl)) var b bytes.Buffer err := t.Execute(&b, vars) if err != nil { diff --git a/sprout.go b/sprout.go index 6fb9778..a517303 100644 --- a/sprout.go +++ b/sprout.go @@ -27,6 +27,8 @@ type FunctionHandler struct { ErrHandling ErrHandling errChan chan error Logger *slog.Logger + funcMap template.FuncMap + funcsAlias map[string][]string } // FunctionHandlerOption defines a type for functional options that configure @@ -39,6 +41,8 @@ func NewFunctionHandler(opts ...FunctionHandlerOption) *FunctionHandler { ErrHandling: ErrHandlingReturnDefaultValue, errChan: make(chan error), Logger: slog.New(&slog.TextHandler{}), + funcMap: make(template.FuncMap), + funcsAlias: make(map[string][]string), } for _, opt := range opts { @@ -82,14 +86,18 @@ func WithFunctionHandler(new *FunctionHandler) FunctionHandlerOption { // additional configured functions. // FOR BACKWARD COMPATIBILITY ONLY func FuncMap(opts ...FunctionHandlerOption) template.FuncMap { - parser := NewFunctionHandler(opts...) + fnHandler := NewFunctionHandler(opts...) // BACKWARD COMPATIBILITY // Fallback to FuncMap() to get the unmigrated functions - funcmap := TxtFuncMap() + for k, v := range TxtFuncMap() { + fnHandler.funcMap[k] = v + } // Added migrated functions - funcmap["hello"] = parser.Hello + fnHandler.funcMap["hello"] = fnHandler.Hello - return funcmap + // Register aliases for functions + fnHandler.registerAliases() + return fnHandler.funcMap }