From a7974f9fb80d281cae241a967631a0f9ecafc7d8 Mon Sep 17 00:00:00 2001 From: Atomys Date: Tue, 2 Apr 2024 20:50:19 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20allow=20functions=20aliasing=20?= =?UTF-8?q?=F0=9F=8C=B1=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request introduces the aliasing feature in the update allows for backward compatibility by mapping old function names to their updated versions. This ensures legacy code remains functional while encouraging the adoption of new naming conventions without breaking existing projects. --- .gitignore | 3 ++ alias.go | 72 +++++++++++++++++++++++++++++++++++++++++++++ alias_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++ functions.go | 52 ++++++++++++-------------------- functions_test.go | 6 ++-- sprout.go | 16 +++++++--- 6 files changed, 182 insertions(+), 42 deletions(-) create mode 100644 alias.go create mode 100644 alias_test.go 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 }