1
0
Fork 0

Adding upstream version 0.28.1.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-22 10:57:38 +02:00
parent 88f1d47ab6
commit e28c88ef14
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
933 changed files with 194711 additions and 0 deletions

45
tools/hook/event.go Normal file
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
tools/hook/event_test.go Normal file
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)
}
}

178
tools/hook/hook.go Normal file
View file

@ -0,0 +1,178 @@
package hook
import (
"sort"
"sync"
"github.com/pocketbase/pocketbase/tools/security"
)
// 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 func(T) error
// 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 generic concurrent safe structure for managing event hooks.
//
// When using custom event it must embed the base [hook.Event].
//
// 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
}
// 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 func(e T) error) string {
return h.Bind(&Handler[T]{Func: fn})
}
// Unbind removes one or many hook handler by their id.
func (h *Hook[T]) Unbind(idsToRemove ...string) {
h.mu.Lock()
defer h.mu.Unlock()
for _, id := range idsToRemove {
for i := len(h.handlers) - 1; i >= 0; i-- {
if h.handlers[i].Id == id {
h.handlers = append(h.handlers[:i], h.handlers[i+1:]...)
break // for now stop on the first occurrence since we don't allow handlers with duplicated ids
}
}
}
}
// 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 event as an argument.
//
// Optionally, this method allows also to register additional one off
// handler funcs that will be temporary appended to the handlers queue.
//
// NB! Each hook handler must call event.Next() in order the hook chain to proceed.
func (h *Hook[T]) Trigger(event T, oneOffHandlerFuncs ...func(T) error) error {
h.mu.RLock()
handlers := make([]func(T) error, 0, len(h.handlers)+len(oneOffHandlerFuncs))
for _, handler := range h.handlers {
handlers = append(handlers, handler.Func)
}
handlers = append(handlers, oneOffHandlerFuncs...)
h.mu.RUnlock()
event.setNextFunc(nil) // reset in case the event is being reused
for i := len(handlers) - 1; i >= 0; i-- {
i := i
old := event.nextFunc()
event.setNextFunc(func() error {
event.setNextFunc(old)
return handlers[i](event)
})
}
return event.Next()
}
func generateHookId() string {
return security.PseudorandomString(20)
}

162
tools/hook/hook_test.go Normal file
View file

@ -0,0 +1,162 @@
package hook
import (
"errors"
"testing"
)
func TestHookAddHandlerAndAdd(t *testing.T) {
calls := ""
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)
}
expectedCalls := "45123'6789"
if calls != expectedCalls {
t.Fatalf("Expected calls sequence %q, got %q", expectedCalls, calls)
}
}
func TestHookLength(t *testing.T) {
h := Hook[*Event]{}
if l := h.Length(); l != 0 {
t.Fatalf("Expected 0 hook handlers, got %d", l)
}
h.BindFunc(func(e *Event) error { return e.Next() })
h.BindFunc(func(e *Event) error { return e.Next() })
if l := h.Length(); l != 2 {
t.Fatalf("Expected 2 hook handlers, got %d", l)
}
}
func TestHookUnbind(t *testing.T) {
h := Hook[*Event]{}
calls := ""
id0 := h.BindFunc(func(e *Event) error { calls += "0"; return e.Next() })
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 != 4 {
t.Fatalf("Expected %d handlers, got %d", 4, total)
}
h.Unbind(id1, id0)
if total := len(h.handlers); total != 2 {
t.Fatalf("Expected %d handlers, got %d", 2, total)
}
err := h.Trigger(&Event{}, func(e *Event) error { calls += "4"; return e.Next() })
if err != nil {
t.Fatal(err)
}
expectedCalls := "234"
if calls != expectedCalls {
t.Fatalf("Expected calls sequence %q, got %q", expectedCalls, calls)
}
}
func TestHookUnbindAll(t *testing.T) {
h := Hook[*Event]{}
h.UnbindAll() // should do nothing and not panic
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 %d handlers before UnbindAll, found %d", 2, total)
}
h.UnbindAll()
if total := len(h.handlers); total != 0 {
t.Fatalf("Expected no handlers after UnbindAll, found %d", total)
}
}
func TestHookTriggerErrorPropagation(t *testing.T) {
err := errors.New("test")
scenarios := []struct {
name string
handlers []func(*Event) error
expectedError error
}{
{
"without error",
[]func(*Event) error{
func(e *Event) error { return e.Next() },
func(e *Event) error { return e.Next() },
},
nil,
},
{
"with error",
[]func(*Event) error{
func(e *Event) error { return e.Next() },
func(e *Event) error { e.Next(); return err },
func(e *Event) error { return e.Next() },
},
err,
},
}
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)
}
})
}
}

84
tools/hook/tagged.go Normal file
View file

@ -0,0 +1,84 @@
package hook
import (
"github.com/pocketbase/pocketbase/tools/list"
)
// 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
}
// wrapped local Hook embedded struct to limit the public API surface.
type mainHook[T Tagger] struct {
*Hook[T]
}
// NewTaggedHook creates a new TaggedHook with the provided main hook and optional tags.
func NewTaggedHook[T Tagger](hook *Hook[T], tags ...string) *TaggedHook[T] {
return &TaggedHook[T]{
mainHook[T]{hook},
tags,
}
}
// TaggedHook defines a proxy hook which register handlers that are triggered only
// if the TaggedHook.tags are empty or includes at least one of the event data tag(s).
type TaggedHook[T Tagger] struct {
mainHook[T]
tags []string
}
// CanTriggerOn checks if the current TaggedHook can be triggered with
// the provided event data tags.
//
// 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 tagsToCheck {
if list.ExistInSlice(t, h.tags) {
return true
}
}
return false
}
// Bind registers the provided handler to the current hooks queue.
//
// 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 e.Next()
}
return h.mainHook.Bind(handler)
}
// BindFunc registers a new handler with the specified function.
//
// 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 func(e T) error) string {
return h.mainHook.BindFunc(func(e T) error {
if h.CanTriggerOn(e.Tags()) {
return fn(e)
}
return e.Next()
})
}

84
tools/hook/tagged_test.go Normal file
View file

@ -0,0 +1,84 @@
package hook
import (
"strings"
"testing"
)
type mockTagsEvent struct {
Event
tags []string
}
func (m mockTagsEvent) Tags() []string {
return m.tags
}
func TestTaggedHook(t *testing.T) {
calls := ""
base := &Hook[*mockTagsEvent]{}
base.BindFunc(func(e *mockTagsEvent) error { calls += "f0"; return e.Next() })
hA := NewTaggedHook(base)
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.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.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 {
event *mockTagsEvent
expectedCalls string
}{
{
&mockTagsEvent{},
"a2f0a1",
},
{
&mockTagsEvent{tags: []string{"missing"}},
"a2f0a1",
},
{
&mockTagsEvent{tags: []string{"b2"}},
"b2a2f0a1b1",
},
{
&mockTagsEvent{tags: []string{"c1"}},
"c2a2f0a1c1",
},
{
&mockTagsEvent{tags: []string{"b1", "c2"}},
"c2b2a2f0a1b1c1",
},
}
for _, s := range scenarios {
t.Run(strings.Join(s.event.tags, "_"), func(t *testing.T) {
calls = "" // reset
err := base.Trigger(s.event)
if err != nil {
t.Fatalf("Unexpected trigger error: %v", err)
}
if calls != s.expectedCalls {
t.Fatalf("Expected calls sequence %q, got %q", s.expectedCalls, calls)
}
})
}
}