hooks
packageAPI reference for the hooks
package.
Imports
(6)Discovery
Discovery finds lifecycle methods on structs via reflection.
type Discovery struct
Example
d := hooks.NewDiscovery()
methods := d.Discover(myService, "OnStart")
Methods
Discover finds all methods on v with the given prefix.
Parameters
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
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 finds methods matching any of the given prefixes.
Parameters
Returns
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 checks if a specific method exists.
Parameters
Returns
func (*Discovery) HasMethod(v any, name string) bool
{
val := reflect.ValueOf(v)
method := val.MethodByName(name)
return method.IsValid()
}
Call invokes a method by name with optional arguments.
Parameters
Returns
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
Returns
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 |
NewDiscovery
NewDiscovery creates a hook discovery instance with caching.
Returns
func NewDiscovery() *Discovery
{
return &Discovery{}
}
MethodInfo
MethodInfo holds information about a discovered method.
type MethodInfo struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | |
| Suffix | string | |
| Method | reflect.Method | |
| Value | reflect.Value |
testOrder
type testOrder struct
Methods
func (*testOrder) OnEnterPaid()
{
o.Status = "paid"
}
func (*testOrder) OnEnterShipped()
{
o.Status = "shipped"
}
func (*testOrder) OnExitDraft()
{
// cleanup
}
Returns
func (*testOrder) CanEnterPaid() bool
{
return o.Status == "draft"
}
Fields
| Name | Type | Description |
|---|---|---|
| Status | string |
TestDiscovery_Discover
Parameters
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")
}
}
TestDiscovery_DiscoverAll
Parameters
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"]))
}
}
TestDiscovery_HasMethod
Parameters
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")
}
}
TestDiscovery_Call
Parameters
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")
}
}
TestDiscovery_Call_Missing
Parameters
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")
}
}
TestDiscovery_Caching
Parameters
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")
}
}
TestRunner_BeforeAfter
Parameters
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)
}
}
TestRunner_BeforeAll
Parameters
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)
}
}
TestRunner_ErrorStopsExecution
Parameters
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")
}
}
TestRunner_Clear
Parameters
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")
}
}
Runner
Runner manages execution of lifecycle hooks with before/after patterns.
type Runner struct
Methods
Before registers a function to be called before a specific event.
Parameters
func (*Runner) Before(key string, fn HookFunc)
{
r.before[key] = append(r.before[key], fn)
}
After registers a function to be called after a specific event.
Parameters
func (*Runner) After(key string, fn HookFunc)
{
r.after[key] = append(r.after[key], fn)
}
BeforeAll registers a function to be called before any event.
Parameters
func (*Runner) BeforeAll(fn HookFunc)
{
r.before["*"] = append(r.before["*"], fn)
}
AfterAll registers a function to be called after any event.
Parameters
func (*Runner) AfterAll(fn HookFunc)
{
r.after["*"] = append(r.after["*"], fn)
}
Run executes before hooks, the action, and after hooks.
Parameters
Returns
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)
}
Parameters
Returns
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 returns the underlying discovery instance.
Returns
func (*Runner) Discovery() *Discovery
{
return r.discovery
}
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 |
HookFunc
HookFunc is a function called at a lifecycle event.
type HookFunc func(ctx context.Context, key string, args []any) error
NewRunner
NewRunner creates a hook runner with a shared discovery instance.
Returns
func NewRunner() *Runner
{
return &Runner{
discovery: NewDiscovery(),
before: make(map[string][]HookFunc),
after: make(map[string][]HookFunc),
}
}