merge v0.23.0-rc changes
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package hook
|
||||
|
||||
// Resolver defines a common interface for a Hook event (see [Event]).
|
||||
type Resolver interface {
|
||||
// Next triggers the next handler in the hook's chain (if any).
|
||||
Next() error
|
||||
|
||||
// note: kept only for the generic interface; may get removed in the future
|
||||
nextFunc() func() error
|
||||
setNextFunc(f func() error)
|
||||
}
|
||||
|
||||
var _ Resolver = (*Event)(nil)
|
||||
|
||||
// Event implements [Resolver] and it is intended to be used as a base
|
||||
// Hook event that you can embed in your custom typed event structs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type CustomEvent struct {
|
||||
// hook.Event
|
||||
//
|
||||
// SomeField int
|
||||
// }
|
||||
type Event struct {
|
||||
next func() error
|
||||
}
|
||||
|
||||
// Next calls the next hook handler.
|
||||
func (e *Event) Next() error {
|
||||
if e.next != nil {
|
||||
return e.next()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextFunc returns the function that Next calls.
|
||||
func (e *Event) nextFunc() func() error {
|
||||
return e.next
|
||||
}
|
||||
|
||||
// setNextFunc sets the function that Next calls.
|
||||
func (e *Event) setNextFunc(f func() error) {
|
||||
e.next = f
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package hook
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEventNext(t *testing.T) {
|
||||
calls := 0
|
||||
|
||||
e := Event{}
|
||||
|
||||
if e.nextFunc() != nil {
|
||||
t.Fatalf("Expected nextFunc to be nil")
|
||||
}
|
||||
|
||||
e.setNextFunc(func() error {
|
||||
calls++
|
||||
return nil
|
||||
})
|
||||
|
||||
if e.nextFunc() == nil {
|
||||
t.Fatalf("Expected nextFunc to be non-nil")
|
||||
}
|
||||
|
||||
e.Next()
|
||||
e.Next()
|
||||
|
||||
if calls != 2 {
|
||||
t.Fatalf("Expected %d calls, got %d", 2, calls)
|
||||
}
|
||||
}
|
||||
+138
-85
@@ -1,126 +1,179 @@
|
||||
package hook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
var StopPropagation = errors.New("Event hook propagation stopped")
|
||||
// HandlerFunc defines a hook handler function.
|
||||
type HandlerFunc[T Resolver] func(e T) error
|
||||
|
||||
// Handler defines a hook handler function.
|
||||
type Handler[T any] func(e T) error
|
||||
// Handler defines a single Hook handler.
|
||||
// Multiple handlers can share the same id.
|
||||
// If Id is not explicitly set it will be autogenerated by Hook.Add and Hook.AddHandler.
|
||||
type Handler[T Resolver] struct {
|
||||
// Func defines the handler function to execute.
|
||||
//
|
||||
// Note that users need to call e.Next() in order to proceed with
|
||||
// the execution of the hook chain.
|
||||
Func HandlerFunc[T]
|
||||
|
||||
// handlerPair defines a pair of string id and Handler.
|
||||
type handlerPair[T any] struct {
|
||||
id string
|
||||
handler Handler[T]
|
||||
// Id is the unique identifier of the handler.
|
||||
//
|
||||
// It could be used later to remove the handler from a hook via [Hook.Remove].
|
||||
//
|
||||
// If missing, an autogenerated value will be assigned when adding
|
||||
// the handler to a hook.
|
||||
Id string
|
||||
|
||||
// Priority allows changing the default exec priority of the handler within a hook.
|
||||
//
|
||||
// If 0, the handler will be executed in the same order it was registered.
|
||||
Priority int
|
||||
}
|
||||
|
||||
// Hook defines a concurrent safe structure for handling event hooks
|
||||
// (aka. callbacks propagation).
|
||||
type Hook[T any] struct {
|
||||
mux sync.RWMutex
|
||||
handlers []*handlerPair[T]
|
||||
}
|
||||
|
||||
// PreAdd registers a new handler to the hook by prepending it to the existing queue.
|
||||
// Hook defines a generic concurrent safe structure for managing event hooks.
|
||||
//
|
||||
// Returns an autogenerated hook id that could be used later to remove the hook with Hook.Remove(id).
|
||||
func (h *Hook[T]) PreAdd(fn Handler[T]) string {
|
||||
h.mux.Lock()
|
||||
defer h.mux.Unlock()
|
||||
|
||||
id := generateHookId()
|
||||
|
||||
// minimize allocations by shifting the slice
|
||||
h.handlers = append(h.handlers, nil)
|
||||
copy(h.handlers[1:], h.handlers)
|
||||
h.handlers[0] = &handlerPair[T]{id, fn}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// Add registers a new handler to the hook by appending it to the existing queue.
|
||||
// When using custom a event it must embed the base [hook.Event].
|
||||
//
|
||||
// Returns an autogenerated hook id that could be used later to remove the hook with Hook.Remove(id).
|
||||
func (h *Hook[T]) Add(fn Handler[T]) string {
|
||||
h.mux.Lock()
|
||||
defer h.mux.Unlock()
|
||||
|
||||
id := generateHookId()
|
||||
|
||||
h.handlers = append(h.handlers, &handlerPair[T]{id, fn})
|
||||
|
||||
return id
|
||||
// Example:
|
||||
//
|
||||
// type CustomEvent struct {
|
||||
// hook.Event
|
||||
// SomeField int
|
||||
// }
|
||||
//
|
||||
// h := Hook[*CustomEvent]{}
|
||||
//
|
||||
// h.BindFunc(func(e *CustomEvent) error {
|
||||
// println(e.SomeField)
|
||||
//
|
||||
// return e.Next()
|
||||
// })
|
||||
//
|
||||
// h.Trigger(&CustomEvent{ SomeField: 123 })
|
||||
type Hook[T Resolver] struct {
|
||||
handlers []*Handler[T]
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Remove removes a single hook handler by its id.
|
||||
func (h *Hook[T]) Remove(id string) {
|
||||
h.mux.Lock()
|
||||
defer h.mux.Unlock()
|
||||
// Bind registers the provided handler to the current hooks queue.
|
||||
//
|
||||
// If handler.Id is empty it is updated with autogenerated value.
|
||||
//
|
||||
// If a handler from the current hook list has Id matching handler.Id
|
||||
// then the old handler is replaced with the new one.
|
||||
func (h *Hook[T]) Bind(handler *Handler[T]) string {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
var exists bool
|
||||
|
||||
if handler.Id == "" {
|
||||
handler.Id = generateHookId()
|
||||
|
||||
// ensure that it doesn't exist
|
||||
DUPLICATE_CHECK:
|
||||
for _, existing := range h.handlers {
|
||||
if existing.Id == handler.Id {
|
||||
handler.Id = generateHookId()
|
||||
goto DUPLICATE_CHECK
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// replace existing
|
||||
for i, existing := range h.handlers {
|
||||
if existing.Id == handler.Id {
|
||||
h.handlers[i] = handler
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append new
|
||||
if !exists {
|
||||
h.handlers = append(h.handlers, handler)
|
||||
}
|
||||
|
||||
// sort handlers by Priority, preserving the original order of equal items
|
||||
sort.SliceStable(h.handlers, func(i, j int) bool {
|
||||
return h.handlers[i].Priority < h.handlers[j].Priority
|
||||
})
|
||||
|
||||
return handler.Id
|
||||
}
|
||||
|
||||
// BindFunc is similar to Bind but registers a new handler from just the provided function.
|
||||
//
|
||||
// The registered handler is added with a default 0 priority and the id will be autogenerated.
|
||||
//
|
||||
// If you want to register a handler with custom priority or id use the [Hook.Bind] method.
|
||||
func (h *Hook[T]) BindFunc(fn HandlerFunc[T]) string {
|
||||
return h.Bind(&Handler[T]{Func: fn})
|
||||
}
|
||||
|
||||
// Unbind removes a single hook handler by its id.
|
||||
func (h *Hook[T]) Unbind(id string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
for i := len(h.handlers) - 1; i >= 0; i-- {
|
||||
if h.handlers[i].id == id {
|
||||
if h.handlers[i].Id == id {
|
||||
h.handlers = append(h.handlers[:i], h.handlers[i+1:]...)
|
||||
return
|
||||
break // for now stop on the first occurrence since we don't allow handlers with duplicated ids
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAll removes all registered handlers.
|
||||
func (h *Hook[T]) RemoveAll() {
|
||||
h.mux.Lock()
|
||||
defer h.mux.Unlock()
|
||||
// UnbindAll removes all registered handlers.
|
||||
func (h *Hook[T]) UnbindAll() {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.handlers = nil
|
||||
}
|
||||
|
||||
// Length returns to total number of registered hook handlers.
|
||||
func (h *Hook[T]) Length() int {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
return len(h.handlers)
|
||||
}
|
||||
|
||||
// Trigger executes all registered hook handlers one by one
|
||||
// with the specified `data` as an argument.
|
||||
// with the specified event as an argument.
|
||||
//
|
||||
// Optionally, this method allows also to register additional one off
|
||||
// handlers that will be temporary appended to the handlers queue.
|
||||
//
|
||||
// The execution stops when:
|
||||
// - hook.StopPropagation is returned in one of the handlers
|
||||
// - any non-nil error is returned in one of the handlers
|
||||
func (h *Hook[T]) Trigger(data T, oneOffHandlers ...Handler[T]) error {
|
||||
h.mux.RLock()
|
||||
// NB! Each hook handler must call event.Next() in order the hook chain to proceed.
|
||||
func (h *Hook[T]) Trigger(event T, oneOffHandlers ...HandlerFunc[T]) error {
|
||||
h.mu.RLock()
|
||||
handlers := make([]HandlerFunc[T], 0, len(h.handlers)+len(oneOffHandlers))
|
||||
for _, handler := range h.handlers {
|
||||
handlers = append(handlers, handler.Func)
|
||||
}
|
||||
handlers = append(handlers, oneOffHandlers...)
|
||||
h.mu.RUnlock()
|
||||
|
||||
handlers := make([]*handlerPair[T], 0, len(h.handlers)+len(oneOffHandlers))
|
||||
handlers = append(handlers, h.handlers...)
|
||||
event.setNextFunc(nil) // reset in case the event is being reused
|
||||
|
||||
// append the one off handlers
|
||||
for i, oneOff := range oneOffHandlers {
|
||||
handlers = append(handlers, &handlerPair[T]{
|
||||
id: fmt.Sprintf("@%d", i),
|
||||
handler: oneOff,
|
||||
for i := len(handlers) - 1; i >= 0; i-- {
|
||||
i := i
|
||||
old := event.nextFunc()
|
||||
event.setNextFunc(func() error {
|
||||
event.setNextFunc(old)
|
||||
return handlers[i](event)
|
||||
})
|
||||
}
|
||||
|
||||
// unlock is not deferred to avoid deadlocks in case Trigger
|
||||
// is called recursively by the handlers
|
||||
h.mux.RUnlock()
|
||||
|
||||
for _, item := range handlers {
|
||||
err := item.handler(data)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if errors.Is(err, StopPropagation) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return event.Next()
|
||||
}
|
||||
|
||||
func generateHookId() string {
|
||||
return security.PseudorandomString(8)
|
||||
return security.PseudorandomString(20)
|
||||
}
|
||||
|
||||
+106
-124
@@ -5,175 +5,157 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHookAddAndPreAdd(t *testing.T) {
|
||||
h := Hook[int]{}
|
||||
func TestHookAddHandlerAndAdd(t *testing.T) {
|
||||
calls := ""
|
||||
|
||||
if total := len(h.handlers); total != 0 {
|
||||
t.Fatalf("Expected no handlers, found %d", total)
|
||||
h := Hook[*Event]{}
|
||||
|
||||
h.BindFunc(func(e *Event) error { calls += "1"; return e.Next() })
|
||||
h.BindFunc(func(e *Event) error { calls += "2"; return e.Next() })
|
||||
h3Id := h.BindFunc(func(e *Event) error { calls += "3"; return e.Next() })
|
||||
h.Bind(&Handler[*Event]{
|
||||
Id: h3Id, // should replace 3
|
||||
Func: func(e *Event) error { calls += "3'"; return e.Next() },
|
||||
})
|
||||
h.Bind(&Handler[*Event]{
|
||||
Func: func(e *Event) error { calls += "4"; return e.Next() },
|
||||
Priority: -2,
|
||||
})
|
||||
h.Bind(&Handler[*Event]{
|
||||
Func: func(e *Event) error { calls += "5"; return e.Next() },
|
||||
Priority: -1,
|
||||
})
|
||||
h.Bind(&Handler[*Event]{
|
||||
Func: func(e *Event) error { calls += "6"; return e.Next() },
|
||||
})
|
||||
h.Bind(&Handler[*Event]{
|
||||
Func: func(e *Event) error { calls += "7"; e.Next(); return errors.New("test") }, // error shouldn't stop the chain
|
||||
})
|
||||
|
||||
h.Trigger(
|
||||
&Event{},
|
||||
func(e *Event) error { calls += "8"; return e.Next() },
|
||||
func(e *Event) error { calls += "9"; return nil }, // skip next
|
||||
func(e *Event) error { calls += "10"; return e.Next() },
|
||||
)
|
||||
|
||||
if total := len(h.handlers); total != 7 {
|
||||
t.Fatalf("Expected %d handlers, found %d", 7, total)
|
||||
}
|
||||
|
||||
triggerSequence := ""
|
||||
expectedCalls := "45123'6789"
|
||||
|
||||
f1 := func(data int) error { triggerSequence += "f1"; return nil }
|
||||
f2 := func(data int) error { triggerSequence += "f2"; return nil }
|
||||
f3 := func(data int) error { triggerSequence += "f3"; return nil }
|
||||
f4 := func(data int) error { triggerSequence += "f4"; return nil }
|
||||
|
||||
h.Add(f1)
|
||||
h.Add(f2)
|
||||
h.PreAdd(f3)
|
||||
h.PreAdd(f4)
|
||||
h.Trigger(1)
|
||||
|
||||
if total := len(h.handlers); total != 4 {
|
||||
t.Fatalf("Expected %d handlers, found %d", 4, total)
|
||||
}
|
||||
|
||||
expectedTriggerSequence := "f4f3f1f2"
|
||||
|
||||
if triggerSequence != expectedTriggerSequence {
|
||||
t.Fatalf("Expected trigger sequence %s, got %s", expectedTriggerSequence, triggerSequence)
|
||||
if calls != expectedCalls {
|
||||
t.Fatalf("Expected calls sequence %q, got %q", expectedCalls, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookRemove(t *testing.T) {
|
||||
h := Hook[int]{}
|
||||
func TestHookLength(t *testing.T) {
|
||||
h := Hook[*Event]{}
|
||||
|
||||
h1Called := false
|
||||
h2Called := false
|
||||
if l := h.Length(); l != 0 {
|
||||
t.Fatalf("Expected 0 hook handlers, got %d", l)
|
||||
}
|
||||
|
||||
id1 := h.Add(func(data int) error { h1Called = true; return nil })
|
||||
h.Add(func(data int) error { h2Called = true; return nil })
|
||||
h.BindFunc(func(e *Event) error { return e.Next() })
|
||||
h.BindFunc(func(e *Event) error { return e.Next() })
|
||||
|
||||
h.Remove("missing") // should do nothing and not panic
|
||||
if l := h.Length(); l != 2 {
|
||||
t.Fatalf("Expected 2 hook handlers, got %d", l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookUnbind(t *testing.T) {
|
||||
h := Hook[*Event]{}
|
||||
|
||||
calls := ""
|
||||
|
||||
id1 := h.BindFunc(func(e *Event) error { calls += "1"; return e.Next() })
|
||||
h.BindFunc(func(e *Event) error { calls += "2"; return e.Next() })
|
||||
h.Bind(&Handler[*Event]{
|
||||
Func: func(e *Event) error { calls += "3"; return e.Next() },
|
||||
})
|
||||
|
||||
h.Unbind("missing") // should do nothing and not panic
|
||||
|
||||
if total := len(h.handlers); total != 3 {
|
||||
t.Fatalf("Expected %d handlers, got %d", 3, total)
|
||||
}
|
||||
|
||||
h.Unbind(id1)
|
||||
|
||||
if total := len(h.handlers); total != 2 {
|
||||
t.Fatalf("Expected %d handlers, got %d", 2, total)
|
||||
}
|
||||
|
||||
h.Remove(id1)
|
||||
|
||||
if total := len(h.handlers); total != 1 {
|
||||
t.Fatalf("Expected %d handlers, got %d", 1, total)
|
||||
}
|
||||
|
||||
if err := h.Trigger(1); err != nil {
|
||||
err := h.Trigger(&Event{}, func(e *Event) error { calls += "4"; return e.Next() })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if h1Called {
|
||||
t.Fatalf("Expected hook 1 to be removed and not called")
|
||||
}
|
||||
expectedCalls := "234"
|
||||
|
||||
if !h2Called {
|
||||
t.Fatalf("Expected hook 2 to be called")
|
||||
if calls != expectedCalls {
|
||||
t.Fatalf("Expected calls sequence %q, got %q", expectedCalls, calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookRemoveAll(t *testing.T) {
|
||||
h := Hook[int]{}
|
||||
func TestHookUnbindAll(t *testing.T) {
|
||||
h := Hook[*Event]{}
|
||||
|
||||
h.RemoveAll() // should do nothing and not panic
|
||||
h.UnbindAll() // should do nothing and not panic
|
||||
|
||||
h.Add(func(data int) error { return nil })
|
||||
h.Add(func(data int) error { return nil })
|
||||
h.BindFunc(func(e *Event) error { return nil })
|
||||
h.BindFunc(func(e *Event) error { return nil })
|
||||
|
||||
if total := len(h.handlers); total != 2 {
|
||||
t.Fatalf("Expected 2 handlers before RemoveAll, found %d", total)
|
||||
t.Fatalf("Expected %d handlers before UnbindAll, found %d", 2, total)
|
||||
}
|
||||
|
||||
h.RemoveAll()
|
||||
h.UnbindAll()
|
||||
|
||||
if total := len(h.handlers); total != 0 {
|
||||
t.Fatalf("Expected no handlers after RemoveAll, found %d", total)
|
||||
t.Fatalf("Expected no handlers after UnbindAll, found %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookTrigger(t *testing.T) {
|
||||
err1 := errors.New("demo")
|
||||
err2 := errors.New("demo")
|
||||
func TestHookTriggerErrorPropagation(t *testing.T) {
|
||||
err := errors.New("test")
|
||||
|
||||
scenarios := []struct {
|
||||
handlers []Handler[int]
|
||||
name string
|
||||
handlers []HandlerFunc[*Event]
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
[]Handler[int]{
|
||||
func(data int) error { return nil },
|
||||
func(data int) error { return nil },
|
||||
"without error",
|
||||
[]HandlerFunc[*Event]{
|
||||
func(e *Event) error { return e.Next() },
|
||||
func(e *Event) error { return e.Next() },
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
[]Handler[int]{
|
||||
func(data int) error { return nil },
|
||||
func(data int) error { return err1 },
|
||||
func(data int) error { return err2 },
|
||||
"with error",
|
||||
[]HandlerFunc[*Event]{
|
||||
func(e *Event) error { return e.Next() },
|
||||
func(e *Event) error { e.Next(); return err },
|
||||
func(e *Event) error { return e.Next() },
|
||||
},
|
||||
err1,
|
||||
err,
|
||||
},
|
||||
}
|
||||
|
||||
for i, scenario := range scenarios {
|
||||
h := Hook[int]{}
|
||||
for _, handler := range scenario.handlers {
|
||||
h.Add(handler)
|
||||
}
|
||||
result := h.Trigger(1)
|
||||
if result != scenario.expectedError {
|
||||
t.Fatalf("(%d) Expected %v, got %v", i, scenario.expectedError, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHookTriggerStopPropagation(t *testing.T) {
|
||||
called1 := false
|
||||
f1 := func(data int) error { called1 = true; return nil }
|
||||
|
||||
called2 := false
|
||||
f2 := func(data int) error { called2 = true; return nil }
|
||||
|
||||
called3 := false
|
||||
f3 := func(data int) error { called3 = true; return nil }
|
||||
|
||||
called4 := false
|
||||
f4 := func(data int) error { called4 = true; return StopPropagation }
|
||||
|
||||
called5 := false
|
||||
f5 := func(data int) error { called5 = true; return nil }
|
||||
|
||||
called6 := false
|
||||
f6 := func(data int) error { called6 = true; return nil }
|
||||
|
||||
h := Hook[int]{}
|
||||
h.Add(f1)
|
||||
h.Add(f2)
|
||||
|
||||
result := h.Trigger(123, f3, f4, f5, f6)
|
||||
|
||||
if result != nil {
|
||||
t.Fatalf("Expected nil after StopPropagation, got %v", result)
|
||||
}
|
||||
|
||||
// ensure that the trigger handler were not persisted
|
||||
if total := len(h.handlers); total != 2 {
|
||||
t.Fatalf("Expected 2 handlers, found %d", total)
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
called bool
|
||||
expected bool
|
||||
}{
|
||||
{called1, true},
|
||||
{called2, true},
|
||||
{called3, true},
|
||||
{called4, true}, // StopPropagation
|
||||
{called5, false},
|
||||
{called6, false},
|
||||
}
|
||||
for i, scenario := range scenarios {
|
||||
if scenario.called != scenario.expected {
|
||||
t.Errorf("(%d) Expected %v, got %v", i, scenario.expected, scenario.called)
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
h := Hook[*Event]{}
|
||||
for _, handler := range s.handlers {
|
||||
h.BindFunc(handler)
|
||||
}
|
||||
result := h.Trigger(&Event{})
|
||||
if result != s.expectedError {
|
||||
t.Fatalf("Expected %v, got %v", s.expectedError, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+23
-13
@@ -7,6 +7,8 @@ import (
|
||||
// Tagger defines an interface for event data structs that support tags/groups/categories/etc.
|
||||
// Usually used together with TaggedHook.
|
||||
type Tagger interface {
|
||||
Resolver
|
||||
|
||||
Tags() []string
|
||||
}
|
||||
|
||||
@@ -33,12 +35,14 @@ type TaggedHook[T Tagger] struct {
|
||||
|
||||
// CanTriggerOn checks if the current TaggedHook can be triggered with
|
||||
// the provided event data tags.
|
||||
func (h *TaggedHook[T]) CanTriggerOn(tags []string) bool {
|
||||
//
|
||||
// It returns always true if the hook doens't have any tags.
|
||||
func (h *TaggedHook[T]) CanTriggerOn(tagsToCheck []string) bool {
|
||||
if len(h.tags) == 0 {
|
||||
return true // match all
|
||||
}
|
||||
|
||||
for _, t := range tags {
|
||||
for _, t := range tagsToCheck {
|
||||
if list.ExistInSlice(t, h.tags) {
|
||||
return true
|
||||
}
|
||||
@@ -47,28 +51,34 @@ func (h *TaggedHook[T]) CanTriggerOn(tags []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PreAdd registers a new handler to the hook by prepending it to the existing queue.
|
||||
// Bind registers the provided handler to the current hooks queue.
|
||||
//
|
||||
// The fn handler will be called only if the event data tags satisfy h.CanTriggerOn.
|
||||
func (h *TaggedHook[T]) PreAdd(fn Handler[T]) string {
|
||||
return h.mainHook.PreAdd(func(e T) error {
|
||||
// It is similar to [Hook.Bind] with the difference that the handler
|
||||
// function is invoked only if the event data tags satisfy h.CanTriggerOn.
|
||||
func (h *TaggedHook[T]) Bind(handler *Handler[T]) string {
|
||||
fn := handler.Func
|
||||
|
||||
handler.Func = func(e T) error {
|
||||
if h.CanTriggerOn(e.Tags()) {
|
||||
return fn(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
return h.mainHook.Bind(handler)
|
||||
}
|
||||
|
||||
// Add registers a new handler to the hook by appending it to the existing queue.
|
||||
// BindFunc registers a new handler with the specified function.
|
||||
//
|
||||
// The fn handler will be called only if the event data tags satisfy h.CanTriggerOn.
|
||||
func (h *TaggedHook[T]) Add(fn Handler[T]) string {
|
||||
return h.mainHook.Add(func(e T) error {
|
||||
// It is similar to [Hook.Bind] with the difference that the handler
|
||||
// function is invoked only if the event data tags satisfy h.CanTriggerOn.
|
||||
func (h *TaggedHook[T]) BindFunc(fn HandlerFunc[T]) string {
|
||||
return h.mainHook.BindFunc(func(e T) error {
|
||||
if h.CanTriggerOn(e.Tags()) {
|
||||
return fn(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
return e.Next()
|
||||
})
|
||||
}
|
||||
|
||||
+43
-28
@@ -1,69 +1,84 @@
|
||||
package hook
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type mockTagsData struct {
|
||||
type mockTagsEvent struct {
|
||||
Event
|
||||
tags []string
|
||||
}
|
||||
|
||||
func (m mockTagsData) Tags() []string {
|
||||
func (m mockTagsEvent) Tags() []string {
|
||||
return m.tags
|
||||
}
|
||||
|
||||
func TestTaggedHook(t *testing.T) {
|
||||
triggerSequence := ""
|
||||
calls := ""
|
||||
|
||||
base := &Hook[mockTagsData]{}
|
||||
base.Add(func(data mockTagsData) error { triggerSequence += "f0"; return nil })
|
||||
base := &Hook[*mockTagsEvent]{}
|
||||
base.BindFunc(func(e *mockTagsEvent) error { calls += "f0"; return e.Next() })
|
||||
|
||||
hA := NewTaggedHook(base)
|
||||
hA.Add(func(data mockTagsData) error { triggerSequence += "a1"; return nil })
|
||||
hA.PreAdd(func(data mockTagsData) error { triggerSequence += "a2"; return nil })
|
||||
hA.BindFunc(func(e *mockTagsEvent) error { calls += "a1"; return e.Next() })
|
||||
hA.Bind(&Handler[*mockTagsEvent]{
|
||||
Func: func(e *mockTagsEvent) error { calls += "a2"; return e.Next() },
|
||||
Priority: -1,
|
||||
})
|
||||
|
||||
hB := NewTaggedHook(base, "b1", "b2")
|
||||
hB.Add(func(data mockTagsData) error { triggerSequence += "b1"; return nil })
|
||||
hB.PreAdd(func(data mockTagsData) error { triggerSequence += "b2"; return nil })
|
||||
hB.BindFunc(func(e *mockTagsEvent) error { calls += "b1"; return e.Next() })
|
||||
hB.Bind(&Handler[*mockTagsEvent]{
|
||||
Func: func(e *mockTagsEvent) error { calls += "b2"; return e.Next() },
|
||||
Priority: -2,
|
||||
})
|
||||
|
||||
hC := NewTaggedHook(base, "c1", "c2")
|
||||
hC.Add(func(data mockTagsData) error { triggerSequence += "c1"; return nil })
|
||||
hC.PreAdd(func(data mockTagsData) error { triggerSequence += "c2"; return nil })
|
||||
hC.BindFunc(func(e *mockTagsEvent) error { calls += "c1"; return e.Next() })
|
||||
hC.Bind(&Handler[*mockTagsEvent]{
|
||||
Func: func(e *mockTagsEvent) error { calls += "c2"; return e.Next() },
|
||||
Priority: -3,
|
||||
})
|
||||
|
||||
scenarios := []struct {
|
||||
data mockTagsData
|
||||
expectedSequence string
|
||||
event *mockTagsEvent
|
||||
expectedCalls string
|
||||
}{
|
||||
{
|
||||
mockTagsData{},
|
||||
&mockTagsEvent{},
|
||||
"a2f0a1",
|
||||
},
|
||||
{
|
||||
mockTagsData{[]string{"missing"}},
|
||||
&mockTagsEvent{tags: []string{"missing"}},
|
||||
"a2f0a1",
|
||||
},
|
||||
{
|
||||
mockTagsData{[]string{"b2"}},
|
||||
&mockTagsEvent{tags: []string{"b2"}},
|
||||
"b2a2f0a1b1",
|
||||
},
|
||||
{
|
||||
mockTagsData{[]string{"c1"}},
|
||||
&mockTagsEvent{tags: []string{"c1"}},
|
||||
"c2a2f0a1c1",
|
||||
},
|
||||
{
|
||||
mockTagsData{[]string{"b1", "c2"}},
|
||||
&mockTagsEvent{tags: []string{"b1", "c2"}},
|
||||
"c2b2a2f0a1b1c1",
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
triggerSequence = "" // reset
|
||||
for _, s := range scenarios {
|
||||
t.Run(strings.Join(s.event.tags, "_"), func(t *testing.T) {
|
||||
calls = "" // reset
|
||||
|
||||
err := hA.Trigger(s.data)
|
||||
if err != nil {
|
||||
t.Fatalf("[%d] Unexpected trigger error: %v", i, err)
|
||||
}
|
||||
err := base.Trigger(s.event)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected trigger error: %v", err)
|
||||
}
|
||||
|
||||
if triggerSequence != s.expectedSequence {
|
||||
t.Fatalf("[%d] Expected trigger sequence %s, got %s", i, s.expectedSequence, triggerSequence)
|
||||
}
|
||||
if calls != s.expectedCalls {
|
||||
t.Fatalf("Expected calls sequence %q, got %q", s.expectedCalls, calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user