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")
	}
}
S
struct

Runner

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

pkg/hooks/runner.go:8-12
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)
}
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
T
type

HookFunc

HookFunc is a function called at a lifecycle event.

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

NewRunner

NewRunner creates a hook runner with a shared discovery instance.

Returns

pkg/hooks/runner.go:18-24
func NewRunner() *Runner

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