merge v0.23.0-rc changes

This commit is contained in:
Gani Georgiev
2024-09-29 19:23:19 +03:00
parent ad92992324
commit 844f18cac3
753 changed files with 85141 additions and 63396 deletions
+45
View File
@@ -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
}
+29
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
})
}
}