hooks API

hooks

package

API reference for the hooks package.

S
struct

Discovery

Discovery finds lifecycle methods on structs via reflection.

pkg/hooks/discovery.go:16-18
type Discovery struct

Example

d := hooks.NewDiscovery()
methods := d.Discover(myService, "OnStart")

Methods

Discover
Method

Discover finds all methods on v with the given prefix.

Parameters

v any
prefix string

Returns

func (*Discovery) Discover(v any, prefix string) []MethodInfo
{
	val := reflect.ValueOf(v)
	typ := val.Type()

	cacheKey := typ.String() + ":" + prefix
	if cached, ok := d.cache.Load(cacheKey); ok {
		return d.resolveFromCache(val, cached.([]MethodInfo))
	}

	var methods []MethodInfo

	for i := 0; i < typ.NumMethod(); i++ {
		method := typ.Method(i)
		if strings.HasPrefix(method.Name, prefix) {
			suffix := strings.TrimPrefix(method.Name, prefix)
			methods = append(methods, MethodInfo{
				Name:   method.Name,
				Suffix: suffix,
				Method: method,
			})
		}
	}

	d.cache.Store(cacheKey, methods)
	return d.resolveFromCache(val, methods)
}

Parameters

cached []MethodInfo

Returns

func (*Discovery) resolveFromCache(val reflect.Value, cached []MethodInfo) []MethodInfo
{
	result := make([]MethodInfo, len(cached))
	for i, m := range cached {
		result[i] = MethodInfo{
			Name:   m.Name,
			Suffix: m.Suffix,
			Method: m.Method,
			Value:  val.MethodByName(m.Name),
		}
	}
	return result
}
DiscoverAll
Method

DiscoverAll finds methods matching any of the given prefixes.

Parameters

v any
prefixes ...string

Returns

map[string][]MethodInfo
func (*Discovery) DiscoverAll(v any, prefixes ...string) map[string][]MethodInfo
{
	result := make(map[string][]MethodInfo)
	for _, prefix := range prefixes {
		methods := d.Discover(v, prefix)
		if len(methods) > 0 {
			result[prefix] = methods
		}
	}
	return result
}
HasMethod
Method

HasMethod checks if a specific method exists.

Parameters

v any
name string

Returns

bool
func (*Discovery) HasMethod(v any, name string) bool
{
	val := reflect.ValueOf(v)
	method := val.MethodByName(name)
	return method.IsValid()
}
Call
Method

Call invokes a method by name with optional arguments.

Parameters

v any
name string
args ...any

Returns

[]any
error
func (*Discovery) Call(v any, name string, args ...any) ([]any, error)
{
	val := reflect.ValueOf(v)
	method := val.MethodByName(name)
	if !method.IsValid() {
		return nil, nil
	}

	in := make([]reflect.Value, len(args))
	for i, arg := range args {
		in[i] = reflect.ValueOf(arg)
	}

	out := method.Call(in)
	result := make([]any, len(out))
	for i, v := range out {
		result[i] = v.Interface()
	}
	return result, nil
}

CallWithContext invokes a method with context as first argument.

Parameters

v any
name string
args ...any

Returns

[]any
error
func (*Discovery) CallWithContext(ctx context.Context, v any, name string, args ...any) ([]any, error)
{
	allArgs := make([]any, 0, len(args)+1)
	allArgs = append(allArgs, ctx)
	allArgs = append(allArgs, args...)
	return d.Call(v, name, allArgs...)
}

Fields

Name Type Description
cache sync.Map
F
function

NewDiscovery

NewDiscovery creates a hook discovery instance with caching.

Returns

pkg/hooks/discovery.go:21-23
func NewDiscovery() *Discovery

{
	return &Discovery{}
}
S
struct

MethodInfo

MethodInfo holds information about a discovered method.

pkg/hooks/discovery.go:26-31
type MethodInfo struct

Fields

Name Type Description
Name string
Suffix string
Method reflect.Method
Value reflect.Value
S
struct

testOrder

pkg/hooks/hooks_test.go:9-11
type testOrder struct

Methods

OnEnterPaid
Method
func (*testOrder) OnEnterPaid()
{
	o.Status = "paid"
}
func (*testOrder) OnEnterShipped()
{
	o.Status = "shipped"
}
OnExitDraft
Method
func (*testOrder) OnExitDraft()
{
	// cleanup
}
CanEnterPaid
Method

Returns

bool
func (*testOrder) CanEnterPaid() bool
{
	return o.Status == "draft"
}
Before
Method

Returns

error
func (*testOrder) Before() error
{
	return nil
}

Fields

Name Type Description
Status string
F
function

TestDiscovery_Discover

Parameters

pkg/hooks/hooks_test.go:33-54
func TestDiscovery_Discover(t *testing.T)

{
	d := NewDiscovery()
	order := &testOrder{Status: "draft"}

	methods := d.Discover(order, "OnEnter")

	if len(methods) != 2 {
		t.Fatalf("got %d methods, want 2", len(methods))
	}

	names := make(map[string]bool)
	for _, m := range methods {
		names[m.Name] = true
		if m.Suffix != "Paid" && m.Suffix != "Shipped" {
			t.Errorf("unexpected suffix: %q", m.Suffix)
		}
	}

	if !names["OnEnterPaid"] || !names["OnEnterShipped"] {
		t.Error("missing expected methods")
	}
}
F
function

TestDiscovery_DiscoverAll

Parameters

pkg/hooks/hooks_test.go:56-73
func TestDiscovery_DiscoverAll(t *testing.T)

{
	d := NewDiscovery()
	order := &testOrder{}

	all := d.DiscoverAll(order, "OnEnter", "OnExit", "Can")

	if len(all["OnEnter"]) != 2 {
		t.Errorf("OnEnter: got %d, want 2", len(all["OnEnter"]))
	}

	if len(all["OnExit"]) != 1 {
		t.Errorf("OnExit: got %d, want 1", len(all["OnExit"]))
	}

	if len(all["Can"]) != 1 {
		t.Errorf("Can: got %d, want 1", len(all["Can"]))
	}
}
F
function

TestDiscovery_HasMethod

Parameters

pkg/hooks/hooks_test.go:75-86
func TestDiscovery_HasMethod(t *testing.T)

{
	d := NewDiscovery()
	order := &testOrder{}

	if !d.HasMethod(order, "OnEnterPaid") {
		t.Error("should have OnEnterPaid")
	}

	if d.HasMethod(order, "OnEnterCancelled") {
		t.Error("should not have OnEnterCancelled")
	}
}
F
function

TestDiscovery_Call

Parameters

pkg/hooks/hooks_test.go:88-100
func TestDiscovery_Call(t *testing.T)

{
	d := NewDiscovery()
	order := &testOrder{Status: "draft"}

	_, err := d.Call(order, "OnEnterPaid")
	if err != nil {
		t.Fatal(err)
	}

	if order.Status != "paid" {
		t.Errorf("status: got %q, want %q", order.Status, "paid")
	}
}
F
function

TestDiscovery_Call_Missing

Parameters

pkg/hooks/hooks_test.go:102-113
func TestDiscovery_Call_Missing(t *testing.T)

{
	d := NewDiscovery()
	order := &testOrder{}

	result, err := d.Call(order, "MissingMethod")
	if err != nil {
		t.Fatal(err)
	}
	if result != nil {
		t.Error("call to missing method should return nil")
	}
}
F
function

TestDiscovery_Caching

Parameters

pkg/hooks/hooks_test.go:115-125
func TestDiscovery_Caching(t *testing.T)

{
	d := NewDiscovery()
	order := &testOrder{}

	methods1 := d.Discover(order, "OnEnter")
	methods2 := d.Discover(order, "OnEnter")

	if len(methods1) != len(methods2) {
		t.Error("cached result should match")
	}
}
F
function

TestRunner_BeforeAfter

Parameters

pkg/hooks/hooks_test.go:127-153
func TestRunner_BeforeAfter(t *testing.T)

{
	r := NewRunner()

	var order []string

	r.Before("save", func(ctx context.Context, key string, args []any) error {
		order = append(order, "before")
		return nil
	})

	r.After("save", func(ctx context.Context, key string, args []any) error {
		order = append(order, "after")
		return nil
	})

	err := r.Run(context.Background(), "save", func() error {
		order = append(order, "action")
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}

	if len(order) != 3 || order[0] != "before" || order[1] != "action" || order[2] != "after" {
		t.Errorf("wrong order: %v", order)
	}
}
F
function

TestRunner_BeforeAll

Parameters

pkg/hooks/hooks_test.go:155-170
func TestRunner_BeforeAll(t *testing.T)

{
	r := NewRunner()

	calls := 0
	r.BeforeAll(func(ctx context.Context, key string, args []any) error {
		calls++
		return nil
	})

	r.Run(context.Background(), "event1", func() error { return nil })
	r.Run(context.Background(), "event2", func() error { return nil })

	if calls != 2 {
		t.Errorf("BeforeAll should be called twice, got %d", calls)
	}
}
F
function

TestRunner_ErrorStopsExecution

Parameters

pkg/hooks/hooks_test.go:172-192
func TestRunner_ErrorStopsExecution(t *testing.T)

{
	r := NewRunner()

	r.Before("fail", func(ctx context.Context, key string, args []any) error {
		return errors.New("before error")
	})

	actionCalled := false
	err := r.Run(context.Background(), "fail", func() error {
		actionCalled = true
		return nil
	})

	if err == nil {
		t.Error("expected error from Before hook")
	}

	if actionCalled {
		t.Error("action should not be called when Before hook fails")
	}
}
F
function

TestRunner_Clear

Parameters

pkg/hooks/hooks_test.go:194-207
func TestRunner_Clear(t *testing.T)

{
	r := NewRunner()

	r.Before("test", func(ctx context.Context, key string, args []any) error {
		return errors.New("should not run")
	})

	r.Clear()

	err := r.Run(context.Background(), "test", func() error { return nil })
	if err != nil {
		t.Error("hooks should be cleared")
	}
}
T
type

HookFunc

HookFunc is a function called at a lifecycle event.

pkg/hooks/runner.go:9-9
type HookFunc func(ctx context.Context, key string, args []any) error
S
struct

Runner

Runner manages execution of lifecycle hooks with before/after patterns.

pkg/hooks/runner.go:12-16
type Runner struct

Methods

Before
Method

Before registers a function to be called before a specific event.

Parameters

key string
func (*Runner) Before(key string, fn HookFunc)
{
	r.before[key] = append(r.before[key], fn)
}
After
Method

After registers a function to be called after a specific event.

Parameters

key string
func (*Runner) After(key string, fn HookFunc)
{
	r.after[key] = append(r.after[key], fn)
}
BeforeAll
Method

BeforeAll registers a function to be called before any event.

Parameters

func (*Runner) BeforeAll(fn HookFunc)
{
	r.before["*"] = append(r.before["*"], fn)
}
AfterAll
Method

AfterAll registers a function to be called after any event.

Parameters

func (*Runner) AfterAll(fn HookFunc)
{
	r.after["*"] = append(r.after["*"], fn)
}
Run
Method

Run executes before hooks, the action, and after hooks.

Parameters

key string
action func() error
args ...any

Returns

error
func (*Runner) Run(ctx context.Context, key string, action func() error, args ...any) error
{
	if err := r.runHooks(ctx, key, r.before, args); err != nil {
		return err
	}

	if err := action(); err != nil {
		return err
	}

	return r.runHooks(ctx, key, r.after, args)
}
RunParallel
Method

RunParallel executes hooks concurrently. Each hook runs in its own goroutine. If any hook returns an error, cancellation is propagated via context.

Parameters

key string
args ...any

Returns

error
func (*Runner) RunParallel(ctx context.Context, key string, args ...any) error
{
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	all := append(r.before["*"], r.before[key]...)
	all = append(all, r.after["*"]...)
	all = append(all, r.after[key]...)

	errCh := make(chan error, len(all))
	for _, fn := range all {
		fn := fn
		go func() {
			select {
			case <-ctx.Done():
				errCh <- ctx.Err()
			default:
				errCh <- fn(ctx, key, args)
			}
		}()
	}

	var firstErr error
	for i := 0; i < len(all); i++ {
		if err := <-errCh; err != nil && firstErr == nil {
			firstErr = err
			cancel()
		}
	}
	return firstErr
}
Example
err := r.RunParallel(ctx, "my-event", args)
runHooks
Method

Parameters

key string
hooks map[string][]HookFunc
args []any

Returns

error
func (*Runner) runHooks(ctx context.Context, key string, hooks map[string][]HookFunc, args []any) error
{
	// Global hooks first
	for _, fn := range hooks["*"] {
		if err := fn(ctx, key, args); err != nil {
			return err
		}
	}

	// Specific hooks
	for _, fn := range hooks[key] {
		if err := fn(ctx, key, args); err != nil {
			return err
		}
	}

	return nil
}
Discovery
Method

Discovery returns the underlying discovery instance.

Returns

func (*Runner) Discovery() *Discovery
{
	return r.discovery
}
Clear
Method

Clear removes all registered hooks.

func (*Runner) Clear()
{
	r.before = make(map[string][]HookFunc)
	r.after = make(map[string][]HookFunc)
}

Fields

Name Type Description
discovery *Discovery
before map[string][]HookFunc
after map[string][]HookFunc
F
function

NewRunner

NewRunner creates a hook runner with a shared discovery instance.

Returns

pkg/hooks/runner.go:19-25
func NewRunner() *Runner

{
	return &Runner{
		discovery: NewDiscovery(),
		before:    make(map[string][]HookFunc),
		after:     make(map[string][]HookFunc),
	}
}
F
function

RunWithTimeout

RunWithTimeout runs an action with a context deadline.

Parameters

timeout
fn
func(ctx context.Context) error

Returns

error
pkg/hooks/runner.go:131-135
func RunWithTimeout(ctx context.Context, timeout time.Duration, fn func(ctx context.Context) error) error

{
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
	return fn(ctx)
}

Example

err := hooks.RunWithTimeout(ctx, time.Second, func() error { ... })