Skip to content

Commit

Permalink
feat: allow functions aliasing 🌱 (#3)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
42atomys authored Apr 2, 2024
1 parent 5fb1854 commit a7974f9
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 42 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.env
.vscode/

vendor/
/.glide
72 changes: 72 additions & 0 deletions alias.go
Original file line number Diff line number Diff line change
@@ -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]
}
}
}
75 changes: 75 additions & 0 deletions alias_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
52 changes: 18 additions & 34 deletions functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
ttemplate "text/template"
"time"
Expand Down Expand Up @@ -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,
Expand All @@ -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) },
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 2 additions & 4 deletions functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
16 changes: 12 additions & 4 deletions sprout.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}

0 comments on commit a7974f9

Please sign in to comment.