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) }