diff --git a/alias.go b/alias.go index 37ec083..bcbdf2b 100644 --- a/alias.go +++ b/alias.go @@ -10,11 +10,14 @@ type FunctionAliasMap = map[string][]string // It should be called after all functions and aliases have been added and // inside the Build function in case of using a custom handler. func AssignAliases(h Handler) { - for originalFunction, aliases := range h.Aliases() { + for originalName, aliases := range h.RawAliases() { + fn, exists := h.RawFunctions()[originalName] + if !exists { + continue + } + for _, alias := range aliases { - if fn, ok := h.Functions()[originalFunction]; ok { - h.Functions()[alias] = fn - } + h.RawFunctions()[alias] = fn } } } @@ -39,9 +42,9 @@ func AssignAliases(h Handler) { // // handler := New(WithAlias("originalFunc", "alias1", "alias2")) func WithAlias(originalFunction string, aliases ...string) HandlerOption[*DefaultHandler] { - return func(p *DefaultHandler) { + return func(p *DefaultHandler) error { if len(aliases) == 0 { - return + return nil } if _, ok := p.cachedFuncsAlias[originalFunction]; !ok { @@ -49,6 +52,7 @@ func WithAlias(originalFunction string, aliases ...string) HandlerOption[*Defaul } p.cachedFuncsAlias[originalFunction] = append(p.cachedFuncsAlias[originalFunction], aliases...) + return nil } } @@ -69,7 +73,7 @@ func WithAlias(originalFunction string, aliases ...string) HandlerOption[*Defaul // "originalFunc2": {"alias2_1", "alias2_2"}, // })) func WithAliases(aliases FunctionAliasMap) HandlerOption[*DefaultHandler] { - return func(p *DefaultHandler) { + return func(p *DefaultHandler) error { for originalFunction, aliasList := range aliases { if _, ok := p.cachedFuncsAlias[originalFunction]; !ok { p.cachedFuncsAlias[originalFunction] = make([]string, 0) @@ -77,5 +81,6 @@ func WithAliases(aliases FunctionAliasMap) HandlerOption[*DefaultHandler] { p.cachedFuncsAlias[originalFunction] = append(p.cachedFuncsAlias[originalFunction], aliasList...) } + return nil } } diff --git a/alias_test.go b/alias_test.go index bc65068..321ffee 100644 --- a/alias_test.go +++ b/alias_test.go @@ -12,13 +12,13 @@ import ( // TestWithAlias checks that aliases are correctly added to a function. func TestWithAlias(t *testing.T) { - handler := NewFunctionHandler() + handler := New() originalFunc := "originalFunc" alias1 := "alias1" alias2 := "alias2" // Apply the WithAlias option with two aliases. - WithAlias(originalFunc, alias1, alias2)(handler) + require.NoError(t, WithAlias(originalFunc, alias1, alias2)(handler)) // Check that the aliases were added. assert.Contains(t, handler.cachedFuncsAlias, originalFunc) @@ -28,18 +28,18 @@ func TestWithAlias(t *testing.T) { } func TestWithAlias_Empty(t *testing.T) { - handler := NewFunctionHandler() + handler := New() originalFunc := "originalFunc" // Apply the WithAlias option with no aliases. - WithAlias(originalFunc)(handler) + require.NoError(t, WithAlias(originalFunc)(handler)) // Check that no aliases were added. assert.NotContains(t, handler.cachedFuncsAlias, originalFunc) } func TestWithAliases(t *testing.T) { - handler := NewFunctionHandler() + handler := New() originalFunc1 := "originalFunc1" alias1 := "alias1" alias2 := "alias2" @@ -47,10 +47,10 @@ func TestWithAliases(t *testing.T) { alias3 := "alias3" // Apply the WithAliases option with two sets of aliases. - WithAliases(FunctionAliasMap{ + require.NoError(t, WithAliases(FunctionAliasMap{ originalFunc1: {alias1, alias2}, originalFunc2: {alias3}, - })(handler) + })(handler)) // Check that the aliases were added. assert.Contains(t, handler.cachedFuncsAlias, originalFunc1) @@ -65,7 +65,7 @@ func TestWithAliases(t *testing.T) { // TestRegisterAliases checks that aliases are correctly registered in the function map. func TestRegisterAliases(t *testing.T) { - handler := NewFunctionHandler() + handler := New() originalFunc := "originalFunc" alias1 := "alias1" alias2 := "alias2" @@ -75,7 +75,7 @@ func TestRegisterAliases(t *testing.T) { handler.cachedFuncsMap[originalFunc] = mockFunc // Apply the WithAlias option and then register the aliases. - WithAlias(originalFunc, alias1, alias2)(handler) + require.NoError(t, WithAlias(originalFunc, alias1, alias2)(handler)) AssignAliases(handler) // Check that the aliases are mapped to the same function as the original function in funcsRegistry. @@ -84,7 +84,7 @@ func TestRegisterAliases(t *testing.T) { } func TestAliasesInTemplate(t *testing.T) { - handler := NewFunctionHandler() + handler := New() originalFuncName := "originalFunc" alias1 := "alias1" alias2 := "alias2" @@ -94,7 +94,7 @@ func TestAliasesInTemplate(t *testing.T) { handler.cachedFuncsMap[originalFuncName] = mockFunc // Apply the WithAlias option and then register the aliases. - WithAlias(originalFuncName, alias1, alias2)(handler) + require.NoError(t, WithAlias(originalFuncName, alias1, alias2)(handler)) // Create a template with the aliases. tmpl, err := template.New("test").Funcs(handler.Build()).Parse(`{{originalFunc}} {{alias1}} {{alias2}}`) diff --git a/docs/advanced/how-to-create-a-handler.md b/docs/advanced/how-to-create-a-handler.md index 3e9bf02..8a52e34 100644 --- a/docs/advanced/how-to-create-a-handler.md +++ b/docs/advanced/how-to-create-a-handler.md @@ -11,8 +11,8 @@ The `Handler` interface in Sprout defines the basic methods required to manage r * `Logger() *slog.Logger`: Returns the logger instance used for logging. * `AddRegistry(registry Registry) error`: Adds a single registry to the handler. * `AddRegistries(registries ...Registry) error`: Adds multiple registries to the handler. -* `Functions() FunctionMap`: Returns the map of registered functions. -* `Aliases() FunctionAliasMap`: Returns the map of function aliases. +* `RawFunctions() FunctionMap`: Returns the map of registered functions. +* `RawAliases() FunctionAliasMap`: Returns the map of function aliases. * `Build() FunctionMap`: Builds and returns the complete function map, ready to be used in templates. ### Step 2: Create Your Custom Handler Struct diff --git a/docs/features/loader-system-registry.md b/docs/features/loader-system-registry.md index b67d71c..8d220d5 100644 --- a/docs/features/loader-system-registry.md +++ b/docs/features/loader-system-registry.md @@ -43,6 +43,14 @@ tpl := template.Must( ) ``` +You can also use the option to add registries when initializing the handler: + +```go +handler := sprout.New( + sprout.WithRegistries(reg1.NewRegistry(), reg2.NewRegistry()), +) +``` + This code sets up your project to utilize the functions from your custom registry, making it easy to integrate and extend functionality. ## How to create a registry diff --git a/docs/introduction/getting-started.md b/docs/introduction/getting-started.md index 6b31d1d..a408921 100644 --- a/docs/introduction/getting-started.md +++ b/docs/introduction/getting-started.md @@ -38,19 +38,27 @@ handler := sprout.New() Sprout supports various customization options using handler options: -* **Logger Configuration:** - +* **Logger Configuration:**\ You can customize the logging behavior by providing a custom logger: ```go logger := slog.New(slog.NewTextHandler(os.Stdout)) handler := sprout.New(sprout.WithLogger(logger)) ``` +* **Load Registry:**\ + You can load a registry directly on your handler using the `WithRegistries` option: + + ```go + handler := sprout.New(sprout.WithRegistries(ownregistry.NewRegistry())) + ``` + + See more below or in dedicated page [loader-system-registry.md](../features/loader-system-registry.md "mention"). * **Aliases Management:**\ You can specify your custom aliases directly on your handler: -
handler := sprout.New(sprout.WithAlias("originalFunc", "alias"))
-
+ ```go
+ handler := sprout.New(sprout.WithAlias("originalFunc", "alias"))
+ ```
See more below or in dedicated page [function-aliases.md](../features/function-aliases.md "mention").
* **Notices:**\
@@ -103,6 +111,14 @@ You can also add multiple registries at once:
handler.AddRegistries(conversion.NewRegistry(), std.NewRegistry())
```
+Or add registries directly when initializing the handler:
+
+```go
+handler := sprout.New(
+ sprout.WithRegistries(conversion.NewRegistry(), std.NewRegistry()),
+)
+```
+
### Function Aliases
Sprout supports function aliases, allowing you to call the same function by different names.
@@ -130,7 +146,7 @@ funcs := handler.Build()
tpl := template.New("example").Funcs(funcs).Parse(`{{ hello }}`)
```
-This prepares all registered functions and aliases for use in templates.
+This prepares all registered functions and aliases for use in templates. This also caches the function map for better performance.
### Working with Templates
diff --git a/handler.go b/handler.go
index 97e7d50..a9e0efa 100644
--- a/handler.go
+++ b/handler.go
@@ -25,16 +25,15 @@ type Handler interface {
// processing environment.
AddRegistry(registry Registry) error
- // AddRegistries registers multiple registries into the Handler. This method
- // simplifies the process of adding multiple sets of functionalities into the
- // template engine at once.
- AddRegistries(registries ...Registry) error
-
- // Functions returns the map of registered functions managed by the Handler.
- Functions() FunctionMap
+ // RawFunctions returns the map of registered functions without any alias,
+ // notices or other additional information. This function is useful for
+ // special cases where you need to access raw data from registries.
+ //
+ // ⚠ To access the function map for the template engine use `Build()` instead.
+ RawFunctions() FunctionMap
- // Aliases returns the map of function aliases managed by the Handler.
- Aliases() FunctionAliasMap
+ // RawAliases returns the map of function aliases managed by the Handler.
+ RawAliases() FunctionAliasMap
// Notices returns the list of function notices managed by the Handler.
Notices() []FunctionNotice
@@ -56,6 +55,7 @@ type DefaultHandler struct {
notices []FunctionNotice
wantSafeFuncs bool
+ built bool
cachedFuncsMap FunctionMap
cachedFuncsAlias FunctionAliasMap
@@ -104,26 +104,26 @@ func (dh *DefaultHandler) AddRegistries(registries ...Registry) error {
// Build retrieves the complete suite of functiosn and alias that has been configured
// within this Handler. This handler is ready to be used with template engines
-// that accept FuncMap, such as html/template or text/template.
+// that accept FuncMap, such as html/template or text/template. It will also
+// cache the function map for future use to avoid rebuilding the function map
+// multiple times, so it is safe to call this method multiple times to retrieve
+// the same builded function map.
//
-// NOTE: This will replace the `FuncsMap()`, `TxtFuncMap()` and `HtmlFuncMap()` from sprig
+// NOTE: This replaces the [github.com/Masterminds/sprig.FuncMap],
+// [github.com/Masterminds/sprig.TxtFuncMap] and [github.com/Masterminds/sprig.HtmlFuncMap]
+// from sprig
func (dh *DefaultHandler) Build() FunctionMap {
+ if dh.built {
+ return dh.cachedFuncsMap
+ }
+
AssignAliases(dh) // Ensure all aliases are processed before returning the registry
AssignNotices(dh) // Ensure all notices are processed before returning the registry
-
- // If safe functions are enabled, wrap all functions with a safe wrapper
- // that logs any errors that occur during function execution.
if dh.wantSafeFuncs {
- safeFuncs := make(FunctionMap)
- for funcName, fn := range dh.cachedFuncsMap {
- safeFuncs[safeFuncName(funcName)] = dh.safeWrapper(funcName, fn)
- }
-
- for funcName, fn := range safeFuncs {
- dh.cachedFuncsMap[funcName] = fn
- }
+ AssignSafeFuncs(dh) // Ensure all functions are wrapped with safe functions
}
+ dh.built = true
return dh.cachedFuncsMap
}
@@ -137,7 +137,7 @@ func (dh *DefaultHandler) Logger() *slog.Logger {
return dh.logger
}
-// Functions returns the map of registered functions managed by the DefaultHandler.
+// RawFunctions returns the map of registered functions managed by the DefaultHandler.
//
// ⚠ This function is for special cases where you need to access the function
// map for the template engine use `Build()` instead.
@@ -145,17 +145,17 @@ func (dh *DefaultHandler) Logger() *slog.Logger {
// This function map contains all the functions that have been added to the handler,
// typically for use in templating engines. Each entry in the map associates a function
// name with its corresponding implementation.
-func (dh *DefaultHandler) Functions() FunctionMap {
+func (dh *DefaultHandler) RawFunctions() FunctionMap {
return dh.cachedFuncsMap
}
-// Aliases returns the map of function aliases managed by the DefaultHandler.
+// RawAliases returns the map of function aliases managed by the DefaultHandler.
//
// The alias map allows certain functions to be referenced by multiple names. This
// can be useful in templating environments where different names might be preferred
// for the same underlying function. The alias map associates each original function
// name with a list of aliases that can be used interchangeably.
-func (dh *DefaultHandler) Aliases() FunctionAliasMap {
+func (dh *DefaultHandler) RawAliases() FunctionAliasMap {
return dh.cachedFuncsAlias
}
@@ -171,22 +171,25 @@ func (dh *DefaultHandler) Notices() []FunctionNotice {
// WithLogger sets the logger used by a DefaultHandler.
func WithLogger(l *slog.Logger) HandlerOption[*DefaultHandler] {
- return func(p *DefaultHandler) {
+ return func(p *DefaultHandler) error {
p.logger = l
+ return nil
}
}
// WithHandler updates a DefaultHandler with settings from another DefaultHandler.
// This is useful for copying configurations between handlers.
func WithHandler(new Handler) HandlerOption[*DefaultHandler] {
- return func(fnh *DefaultHandler) {
+ return func(fnh *DefaultHandler) error {
if new == nil {
- return
+ return nil
}
if fhCast, ok := new.(*DefaultHandler); ok {
*fnh = *fhCast
}
+
+ return nil
}
}
@@ -198,19 +201,39 @@ func WithHandler(new Handler) HandlerOption[*DefaultHandler] {
// To use a safe function, prepend `safe` to the original function name,
// example: `safeOriginalFuncName` instead of `originalFuncName`.
func WithSafeFuncs(enabled bool) HandlerOption[*DefaultHandler] {
- return func(dh *DefaultHandler) {
+ return func(dh *DefaultHandler) error {
dh.wantSafeFuncs = enabled
+ return nil
+ }
+}
+
+// AssignSafeFuncs wraps all functions with a safe wrapper that logs any errors
+// that occur during function execution. If safe functions are enabled in the
+// DefaultHandler, this method will prepend "safe" to the function name and
+// create a safe wrapper for each function.
+//
+// E.G. all functions will have both the original function name and a safe function name:
+//
+// originalFuncName -> SafeOriginalFuncName
+func AssignSafeFuncs(handler Handler) {
+ safeFuncs := make(FunctionMap)
+ for funcName, fn := range handler.RawFunctions() {
+ safeFuncs[safeFuncName(funcName)] = safeWrapper(handler, funcName, fn)
+ }
+
+ for funcName, fn := range safeFuncs {
+ handler.RawFunctions()[funcName] = fn
}
}
// safeWrapper create a safe wrapper function that calls the original function
// and logs any errors that occur during the function call without interrupting
// the execution of the template.
-func (dh *DefaultHandler) safeWrapper(functionName string, fn any) wrappedFunc {
+func safeWrapper(handler Handler, functionName string, fn any) wrappedFunction {
return func(args ...any) (any, error) {
out, err := runtime.SafeCall(fn, args...)
if err != nil {
- dh.Logger().With("function", functionName, "error", err).Error("function call failed")
+ handler.Logger().With("function", functionName, "error", err).Error("function call failed")
}
return out, nil
}
diff --git a/handler_test.go b/handler_test.go
index 03416d7..a3b36a0 100644
--- a/handler_test.go
+++ b/handler_test.go
@@ -269,12 +269,12 @@ func TestDefaultHandler_Registries(t *testing.T) {
assert.Len(t, dh.registries, 2, "Registries should return the correct number of registries")
}
-// TestDefaultHandler_Functions tests the Functions method of DefaultHandler.
-func TestDefaultHandler_Functions(t *testing.T) {
+// TestDefaultHandler_RawFunctions tests the Functions method of DefaultHandler.
+func TestDefaultHandler_RawFunctions(t *testing.T) {
funcsMap := make(FunctionMap)
dh := &DefaultHandler{cachedFuncsMap: funcsMap}
- assert.Equal(t, funcsMap, dh.Functions(), "Functions should return the correct FunctionMap")
+ assert.Equal(t, funcsMap, dh.RawFunctions(), "Functions should return the correct FunctionMap")
}
// TestDefaultHandler_Aliases tests the Aliases method of DefaultHandler.
@@ -282,7 +282,7 @@ func TestDefaultHandler_Aliases(t *testing.T) {
aliasesMap := make(FunctionAliasMap)
dh := &DefaultHandler{cachedFuncsAlias: aliasesMap}
- assert.Equal(t, aliasesMap, dh.Aliases(), "Aliases should return the correct FunctionAliasMap")
+ assert.Equal(t, aliasesMap, dh.RawAliases(), "Aliases should return the correct FunctionAliasMap")
}
// TestDefaultHandler_Build tests the Build method of DefaultHandler.
@@ -301,6 +301,9 @@ func TestDefaultHandler_Build(t *testing.T) {
builtFuncsMap := dh.Build()
assert.Equal(t, funcsMap, builtFuncsMap, "Build should return the correct FunctionMap")
+
+ builtFuncsMapSecond := dh.Build()
+ assert.Equal(t, builtFuncsMap, builtFuncsMapSecond, "Build should return the same FunctionMap on subsequent calls")
}
func TestDefaultHandler_safeWrapper(t *testing.T) {
@@ -311,7 +314,7 @@ func TestDefaultHandler_safeWrapper(t *testing.T) {
_, err := fn()
require.Error(t, err, "fn should return an error")
- safeFn := handler.safeWrapper("fn", fn)
+ safeFn := safeWrapper(handler, "fn", fn)
_, safeErr := safeFn()
require.NoError(t, safeErr, "safeFn should not return an error")
assert.Equal(t, "[ERROR] function call failed\n", loggerHandler.messages.String())
diff --git a/notice.go b/notice.go
index 1a1f93c..6fe2978 100644
--- a/notice.go
+++ b/notice.go
@@ -7,12 +7,6 @@ import (
"github.com/go-sprout/sprout/internal/runtime"
)
-// wrappedFunc is a type alias for a function that accepts a variadic number of
-// arguments of any type and returns a single result of any type along with an
-// error. This is typically used for functions that need to be wrapped with
-// additional logic, such as logging or notice handling.
-type wrappedFunc = func(args ...any) (any, error)
-
// NoticeKind represents the type of notice that can be applied to a function.
// It is an enumeration with different possible values that dictate how the
// notice should behave.
@@ -91,23 +85,23 @@ func NewDebugNotice(functionName, message string) *FunctionNotice {
// It should be called after all functions and notices have been added and
// inside the Build function in case of using a custom handler.
func AssignNotices(h Handler) {
- funcs := h.Functions()
+ funcs := h.RawFunctions()
for _, notice := range h.Notices() {
for _, functionName := range notice.FunctionNames {
if fn, ok := funcs[functionName]; ok {
- wrappedFn := createWrappedFunction(h, notice, functionName, fn)
+ wrappedFn := noticeWrapper(h, notice, functionName, fn)
funcs[functionName] = wrappedFn
}
}
}
}
-// createWrappedFunction creates a wrapped function that logs a notice after
+// noticeWrapper creates a wrapped function that logs a notice after
// calling the original function. The notice is logged using the handler's
// logger instance. The wrapped function is returned as a wrappedFunc, which
// is a type alias for a function that takes a variadic list of arguments
// and returns an `any` result and an `error`.
-func createWrappedFunction(h Handler, notice FunctionNotice, functionName string, fn any) wrappedFunc {
+func noticeWrapper(h Handler, notice FunctionNotice, functionName string, fn any) wrappedFunction {
return func(args ...any) (any, error) {
out, err := runtime.SafeCall(fn, args...)
switch notice.Kind {
@@ -130,7 +124,7 @@ func createWrappedFunction(h Handler, notice FunctionNotice, functionName string
// You can use the ApplyOnAliases method on the FunctionNotice to control
// whether the notice should be applied to aliases.
func WithNotices(notices ...*FunctionNotice) HandlerOption[*DefaultHandler] {
- return func(p *DefaultHandler) {
+ return func(p *DefaultHandler) error {
// Preallocate the slice if we expect to append multiple notices
if cap(p.notices) < len(p.notices)+len(notices) {
newNotices := make([]FunctionNotice, len(p.notices), len(p.notices)+len(notices))
@@ -147,5 +141,7 @@ func WithNotices(notices ...*FunctionNotice) HandlerOption[*DefaultHandler] {
// Append the notice directly without dereferencing
p.notices = append(p.notices, *notice)
}
+
+ return nil
}
}
diff --git a/notice_test.go b/notice_test.go
index 36c216a..bc183e4 100644
--- a/notice_test.go
+++ b/notice_test.go
@@ -41,7 +41,7 @@ func TestWithNotice(t *testing.T) {
notice := NewInfoNotice(originalFunc, "amazing")
// Apply the WithNotices option with one notice.
- WithNotices(notice)(handler)
+ require.NoError(t, WithNotices(notice)(handler))
// Check that the aliases were added.
assert.Contains(t, handler.Notices(), *notice)
@@ -49,7 +49,7 @@ func TestWithNotice(t *testing.T) {
// Apply the WithNotices option with multiple notices.
notice2 := NewDeprecatedNotice(originalFunc, "oh no")
- WithNotices(notice, notice2)(handler)
+ require.NoError(t, WithNotices(notice, notice2)(handler))
// Check that the aliases were added.
assert.Contains(t, handler.Notices(), *notice)
@@ -58,7 +58,7 @@ func TestWithNotice(t *testing.T) {
// Apply the WithNotices option with an empty message
notice3 := NewDebugNotice(originalFunc, "")
- WithNotices(notice3)(handler)
+ require.NoError(t, WithNotices(notice3)(handler))
assert.Contains(t, handler.Notices(), *notice)
assert.Contains(t, handler.Notices(), *notice2)
@@ -67,7 +67,7 @@ func TestWithNotice(t *testing.T) {
// Try to apply a notice with an empty function name.
notice4 := &FunctionNotice{}
- WithNotices(notice4)(handler)
+ require.NoError(t, WithNotices(notice4)(handler))
// Check that the aliases were not added.
assert.NotContains(t, handler.Notices(), *notice4)
@@ -91,8 +91,8 @@ func TestAssignNotices(t *testing.T) {
assert.Contains(t, handler.Notices(), *notice)
assert.Len(t, handler.notices, 1, "there should be exactly 1 notice")
- require.Contains(t, handler.Functions(), originalFunc)
- assert.NotEqual(t, reflect.ValueOf(mockFunc).Pointer(), reflect.ValueOf(handler.Functions()[originalFunc]).Pointer(), "the function should have been wrapped")
+ require.Contains(t, handler.RawFunctions(), originalFunc)
+ assert.NotEqual(t, reflect.ValueOf(mockFunc).Pointer(), reflect.ValueOf(handler.RawFunctions()[originalFunc]).Pointer(), "the function should have been wrapped")
}
func TestCreateWrappedFunction(t *testing.T) {
@@ -103,9 +103,9 @@ func TestCreateWrappedFunction(t *testing.T) {
mockFunc := func() string { return "cheese" }
// Create a wrapped function.
- wrappedFunc := createWrappedFunction(handler, *NewInfoNotice(originalFunc, "amazing"), originalFunc, mockFunc)
- wrappedFunc2 := createWrappedFunction(handler, *NewDeprecatedNotice(originalFunc, "oh no"), originalFunc, mockFunc)
- wrappedFunc3 := createWrappedFunction(handler, *NewNotice(NoticeKindDebug, []string{originalFunc}, "Nice this function returns $out"), originalFunc, mockFunc)
+ wrappedFunc := noticeWrapper(handler, *NewInfoNotice(originalFunc, "amazing"), originalFunc, mockFunc)
+ wrappedFunc2 := noticeWrapper(handler, *NewDeprecatedNotice(originalFunc, "oh no"), originalFunc, mockFunc)
+ wrappedFunc3 := noticeWrapper(handler, *NewNotice(NoticeKindDebug, []string{originalFunc}, "Nice this function returns $out"), originalFunc, mockFunc)
// Call the wrapped function.
out, err := wrappedFunc()
diff --git a/registry.go b/registry.go
index c86ed58..f0bcd57 100644
--- a/registry.go
+++ b/registry.go
@@ -68,3 +68,16 @@ func AddAlias(aliasMap FunctionAliasMap, originalFunction string, aliases ...str
func AddNotice(notices *[]FunctionNotice, notice *FunctionNotice) {
*notices = append(*notices, *notice)
}
+
+// WithRegistries returns a HandlerOption that adds the provided registries to the handler.
+// This option simplifies the process of adding multiple sets of functionalities into the
+// template engine at once.
+//
+// Example:
+//
+// handler := New(WithRegistries(myRegistry1, myRegistry2, myRegistry3))
+func WithRegistries(registries ...Registry) HandlerOption[*DefaultHandler] {
+ return func(dh *DefaultHandler) error {
+ return dh.AddRegistries(registries...)
+ }
+}
diff --git a/registry_test.go b/registry_test.go
index d4a5258..1fa1a19 100644
--- a/registry_test.go
+++ b/registry_test.go
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
)
func TestAddFunction(t *testing.T) {
@@ -53,3 +54,25 @@ func TestAddAlias(t *testing.T) {
AddAlias(aliasMap, "nonExistentFunc", "aliasX")
assert.Contains(t, aliasMap, "nonExistentFunc", "Aliases should be added under 'nonExistentFunc' even if the function doesn't exist")
}
+
+func TestWithRegistries(t *testing.T) {
+ // Define two registries with functions and aliases
+ mockRegistry1 := new(MockRegistry)
+ mockRegistry1.On("Uid").Return("mockRegistry1")
+ mockRegistry1.On("LinkHandler", mock.Anything).Return(nil)
+ mockRegistry1.On("RegisterFunctions", mock.Anything).Return(nil)
+
+ mockRegistry2 := new(MockRegistry)
+ mockRegistry2.linkHandlerMustCrash = true
+ mockRegistry2.On("Uid").Return("mockRegistry2")
+ mockRegistry2.On("LinkHandler", mock.Anything).Return(nil)
+ mockRegistry1.On("RegisterFunctions", mock.Anything).Return(nil)
+
+ // Create a handler with the registries
+ handler := New(WithRegistries(mockRegistry1, mockRegistry2))
+ handler.Build()
+
+ // Check that the functions and aliases are present in the handler
+ assert.Contains(t, handler.registries, mockRegistry1, "Registry 1 should be added to the handler")
+ assert.Contains(t, handler.registries, mockRegistry2, "Registry 2 should be added to the handler")
+}
diff --git a/sprigin/sprig_backward_compatibility.go b/sprigin/sprig_backward_compatibility.go
index 2224437..d01ec9c 100644
--- a/sprigin/sprig_backward_compatibility.go
+++ b/sprigin/sprig_backward_compatibility.go
@@ -144,11 +144,11 @@ func (sh *SprigHandler) Logger() *slog.Logger {
return slog.New(slog.Default().Handler())
}
-func (sh *SprigHandler) Functions() sprout.FunctionMap {
+func (sh *SprigHandler) RawFunctions() sprout.FunctionMap {
return sh.funcsMap
}
-func (sh *SprigHandler) Aliases() sprout.FunctionAliasMap {
+func (sh *SprigHandler) RawAliases() sprout.FunctionAliasMap {
return sh.funcsAlias
}
diff --git a/sprigin/sprig_backward_compatibility_test.go b/sprigin/sprig_backward_compatibility_test.go
index a210805..49dfc4b 100644
--- a/sprigin/sprig_backward_compatibility_test.go
+++ b/sprigin/sprig_backward_compatibility_test.go
@@ -53,8 +53,8 @@ func TestSprigHandler(t *testing.T) {
handler.Build()
- assert.GreaterOrEqual(t, len(handler.Functions()), sprigFunctionCount)
- assert.Len(t, handler.Aliases(), 37) // Hardcoded for backward compatibility
+ assert.GreaterOrEqual(t, len(handler.RawFunctions()), sprigFunctionCount)
+ assert.Len(t, handler.RawAliases(), 37) // Hardcoded for backward compatibility
assert.Len(t, handler.registries, 18) // Hardcoded for backward compatibility
diff --git a/sprout.go b/sprout.go
index f2ab31b..88398ec 100644
--- a/sprout.go
+++ b/sprout.go
@@ -7,7 +7,13 @@ import (
// HandlerOption[Handler] defines a type for functional options that configure
// a typed Handler.
-type HandlerOption[T Handler] func(T)
+type HandlerOption[T Handler] func(T) error
+
+// wrappedFunction is a type alias for a function that accepts a variadic number of
+// arguments of any type and returns a single result of any type along with an
+// error. This is typically used for functions that need to be wrapped with
+// additional logic, such as logging or notice handling.
+type wrappedFunction = func(args ...any) (any, error)
// New creates and returns a new instance of DefaultHandler with optional
// configurations.
@@ -22,7 +28,7 @@ type HandlerOption[T Handler] func(T)
// logger := slog.New(slog.NewTextHandler(os.Stdout))
// handler := New(
// WithLogger(logger),
-// WithRegistry(myRegistry),
+// WithRegistries(myRegistry),
// )
//
// In the above example, the DefaultHandler is created with a custom logger and
@@ -40,15 +46,10 @@ func New(opts ...HandlerOption[*DefaultHandler]) *DefaultHandler {
}
for _, opt := range opts {
- opt(dh)
+ if err := opt(dh); err != nil {
+ dh.logger.With("error", err).Error("Failed to apply handler option")
+ }
}
return dh
}
-
-// Deprecated: NewFunctionHandler creates a new function handler with the
-// default values. It is deprecated and should not be used. Use `New` instead.
-func NewFunctionHandler(opts ...HandlerOption[*DefaultHandler]) *DefaultHandler {
- slog.Warn("NewFunctionHandler are deprecated. Use `New` instead")
- return New(opts...)
-}
diff --git a/sprout_test.go b/sprout_test.go
index 589e3c4..1f8aa65 100644
--- a/sprout_test.go
+++ b/sprout_test.go
@@ -6,18 +6,19 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
-func TestNewFunctionHandler_DefaultValues(t *testing.T) {
- handler := NewFunctionHandler()
+func TestNew_DefaultValues(t *testing.T) {
+ handler := New()
assert.NotNil(t, handler)
assert.NotNil(t, handler.Logger)
}
-func TestNewFunctionHandler_CustomValues(t *testing.T) {
+func TestNew_CustomValues(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
- handler := NewFunctionHandler(
+ handler := New(
WithLogger(logger),
)
@@ -29,8 +30,8 @@ func TestWithLogger(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
option := WithLogger(logger)
- handler := NewFunctionHandler()
- option(handler) // Apply the option
+ handler := New()
+ require.NoError(t, option(handler)) // Apply the option
assert.Equal(t, logger, handler.Logger())
}
@@ -42,7 +43,7 @@ func TestWithParser(t *testing.T) {
option := WithHandler(fnHandler)
handler := New()
- option(handler) // Apply the option
+ require.NoError(t, option(handler)) // Apply the option
assert.Equal(t, fnHandler, handler)
}
@@ -54,7 +55,7 @@ func TestWithNilHandler(t *testing.T) {
option := WithHandler(nil)
beforeApply := fnHandler
- option(beforeApply)
+ require.NoError(t, option(beforeApply)) // Apply the option
assert.Equal(t, beforeApply, fnHandler)
}
@@ -64,13 +65,13 @@ func TestWithSafeFuncs(t *testing.T) {
assert.True(t, handler.wantSafeFuncs)
handler.cachedFuncsMap["test"] = func() {}
- funcCount := len(handler.Functions())
+ funcCount := len(handler.RawFunctions())
handler.Build()
assert.Len(t, handler.cachedFuncsMap, funcCount*2)
var keys []string
- for k := range handler.Functions() {
+ for k := range handler.RawFunctions() {
keys = append(keys, k)
}