Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolve issues with the v0.6.0-rc.1 #68

Merged
merged 15 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
// 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() {
_, exists := h.RawFunctions()[originalName]
if !exists {
continue

Check warning on line 16 in alias.go

View check run for this annotation

Codecov / codecov/patch

alias.go#L16

Added line #L16 was not covered by tests
}

for _, alias := range aliases {
if fn, ok := h.Functions()[originalFunction]; ok {
h.Functions()[alias] = fn
if fn, ok := h.RawFunctions()[originalName]; ok {
h.RawFunctions()[alias] = fn
}
}
}
Expand All @@ -39,16 +44,17 @@
//
// 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 {
p.cachedFuncsAlias[originalFunction] = make([]string, 0)
}

p.cachedFuncsAlias[originalFunction] = append(p.cachedFuncsAlias[originalFunction], aliases...)
return nil
}
}

Expand All @@ -69,13 +75,14 @@
// "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)
}

p.cachedFuncsAlias[originalFunction] = append(p.cachedFuncsAlias[originalFunction], aliasList...)
}
return nil
}
}
22 changes: 11 additions & 11 deletions alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -28,29 +28,29 @@ 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"
originalFunc2 := "originalFunc2"
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)
Expand All @@ -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"
Expand All @@ -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.
Expand All @@ -84,7 +84,7 @@ func TestRegisterAliases(t *testing.T) {
}

func TestAliasesInTemplate(t *testing.T) {
handler := NewFunctionHandler()
handler := New()
originalFuncName := "originalFunc"
alias1 := "alias1"
alias2 := "alias2"
Expand All @@ -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}}`)
Expand Down
4 changes: 2 additions & 2 deletions docs/advanced/how-to-create-a-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions docs/features/loader-system-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ tpl := template.Must(
)
```

You can also use the option to add registries when initializing the handler:

```go
handler := sprout.New(
// Add one registry
sprout.WithRegistry(ownregistry.NewRegistry()),
// Add more than one at the same time
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
Expand Down
26 changes: 21 additions & 5 deletions docs/introduction/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `WithRegistry` option:

```go
handler := sprout.New(sprout.WithRegistry(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:

<pre class="language-go"><code class="lang-go"><strong>handler := sprout.New(sprout.WithAlias("originalFunc", "alias"))
</strong></code></pre>
```go
handler := sprout.New(sprout.WithAlias("originalFunc", "alias"))
```

See more below or in dedicated page [function-aliases.md](../features/function-aliases.md "mention").
* **Notices:**\
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
83 changes: 51 additions & 32 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -56,6 +55,7 @@ type DefaultHandler struct {
notices []FunctionNotice

wantSafeFuncs bool
built bool

cachedFuncsMap FunctionMap
cachedFuncsAlias FunctionAliasMap
Expand Down Expand Up @@ -104,26 +104,22 @@ 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
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
dh.buildSafeFuncs()

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

dh.built = true
return dh.cachedFuncsMap
}

Expand All @@ -137,25 +133,25 @@ 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.
//
// 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
}

Expand All @@ -171,22 +167,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
}
}

Expand All @@ -198,15 +197,16 @@ 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
}
}

// 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 (dh *DefaultHandler) safeWrapper(functionName string, fn any) wrappedFunction {
return func(args ...any) (any, error) {
out, err := runtime.SafeCall(fn, args...)
if err != nil {
Expand Down Expand Up @@ -235,3 +235,22 @@ func safeFuncName(name string) string {

return b.String()
}

// buildSafeFuncs 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.
func (dh *DefaultHandler) buildSafeFuncs() {
if !dh.wantSafeFuncs {
return
}

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