actions
packageAPI reference for the actions
package.
Imports
(12)context
STD
testing
STD
errors
STD
fmt
STD
reflect
STD
sort
STD
strings
STD
sync
INT
github.com/mirkobrombin/go-foundation/v2/app/di
INT
github.com/mirkobrombin/go-foundation/v2/core/events
INT
github.com/mirkobrombin/go-foundation/v2/core/options
INT
github.com/mirkobrombin/go-foundation/v2/core/reflectutil
renameInput
type renameInput struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string |
renameResult
type renameResult struct
Fields
| Name | Type | Description |
|---|---|---|
| Path | string |
TestTypedAction
Parameters
func TestTypedAction(t *testing.T)
{
action := NewTyped[renameInput, renameResult]("file.rename")
router := New()
err := HandleTyped(router, action, func(_ context.Context, input renameInput) (renameResult, error) {
return renameResult{Path: input.Name}, nil
})
if err != nil {
t.Fatalf("HandleTyped() error = %v", err)
}
got, err := DispatchTyped(context.Background(), router, action, renameInput{Name: "new.md"})
if err != nil {
t.Fatalf("DispatchTyped() error = %v", err)
}
if got.Path != "new.md" {
t.Fatalf("DispatchTyped() = %#v", got)
}
if !router.Has(action.Name()) {
t.Fatal("Has() did not report a typed action")
}
}
TestTypedActionRejectsRuntimeTypeMismatch
Parameters
func TestTypedActionRejectsRuntimeTypeMismatch(t *testing.T)
{
action := NewTyped[renameInput, renameResult]("file.rename")
router := New()
if err := HandleTyped(router, action, func(_ context.Context, input renameInput) (renameResult, error) {
return renameResult{Path: input.Name}, nil
}); err != nil {
t.Fatalf("HandleTyped() error = %v", err)
}
if _, err := router.Dispatch(context.Background(), action.Name(), "wrong"); err == nil {
t.Fatal("Dispatch() accepted the wrong payload type")
}
}
Handler
Handler is the interface for declarative actions.
type Handler interface
Methods
Definition
Definition describes an action without runtime metadata discovery.
type Definition struct
Fields
| Name | Type | Description |
|---|---|---|
| Name | string | |
| Key | string | |
| New | func() Handler |
actionMeta
type actionMeta struct
Fields
| Name | Type | Description |
|---|---|---|
| name | string | |
| key | string | |
| new | func() Handler |
Router
Router dispatches named actions and optional key bindings.
type Router struct
Methods
UseContainer replaces the DI container used during dispatch.
Parameters
func (*Router) UseContainer(container *di.Container)
{
r.mu.Lock()
r.container = container
r.mu.Unlock()
}
UseEvents emits the action instance after dispatch.
Parameters
func (*Router) UseEvents(bus *events.Bus)
{
r.mu.Lock()
r.bus = bus
r.asyncEvents = false
r.mu.Unlock()
}
UseAsyncEvents emits the action instance asynchronously after dispatch.
Parameters
func (*Router) UseAsyncEvents(bus *events.Bus)
{
r.mu.Lock()
r.bus = bus
r.asyncEvents = true
r.mu.Unlock()
}
Provide registers a named dependency for action handlers.
Parameters
func (*Router) Provide(name string, instance any)
{
r.mu.Lock()
defer r.mu.Unlock()
if r.builder == nil {
r.builder = di.NewBuilder()
}
r.builder.Provide(name, instance)
if r.container != nil {
r.container.Provide(name, instance)
}
}
Build constructs the DI container used during dispatch.
Returns
func (*Router) Build() (*di.Container, error)
{
r.mu.Lock()
defer r.mu.Unlock()
if r.container != nil {
return r.container, nil
}
if r.builder == nil {
r.builder = di.NewBuilder()
}
container, err := r.builder.Build()
if err != nil {
return nil, err
}
r.container = container
return container, nil
}
Register registers an action handler.
Parameters
func (*Router) Register(prototype Handler)
{
meta := parseAction(prototype)
if err := r.register(meta); err != nil {
panic(err)
}
}
RegisterDefinition registers a statically described action.
Parameters
Returns
func (*Router) RegisterDefinition(def Definition) error
{
return r.RegisterDefinitions(def)
}
RegisterDefinitions validates and registers static actions as one operation.
Parameters
Returns
func (*Router) RegisterDefinitions(defs ...Definition) error
{
r.mu.Lock()
defer r.mu.Unlock()
metas, err := r.validateDefinitionsLocked(r.container, defs)
if err != nil {
return err
}
for _, meta := range metas {
r.handlers[meta.name] = meta
if meta.key != "" {
r.keys[meta.key] = meta.name
}
}
return nil
}
ValidateDefinitions checks static actions without changing the router.
Parameters
Returns
func (*Router) ValidateDefinitions(container *di.Container, defs ...Definition) error
{
r.mu.RLock()
defer r.mu.RUnlock()
_, err := r.validateDefinitionsLocked(container, defs)
return err
}
Parameters
Returns
func (*Router) validateDefinitionsLocked(container *di.Container, defs []Definition) ([]actionMeta, error)
{
names := make(map[string]struct{}, len(r.handlers)+len(r.typed)+len(defs))
for name := range r.handlers {
names[name] = struct{}{}
}
for name := range r.typed {
names[name] = struct{}{}
}
keys := make(map[string]string, len(r.keys)+len(defs))
for key, name := range r.keys {
keys[key] = name
}
metas := make([]actionMeta, 0, len(defs))
for _, def := range defs {
if def.Name == "" {
return nil, fmt.Errorf("actions: definition requires a name")
}
if def.New == nil {
return nil, fmt.Errorf("actions: definition requires a constructor")
}
instance := def.New()
if instance == nil {
return nil, fmt.Errorf("actions: constructor returned nil")
}
if container != nil {
if err := container.ValidateTarget(instance); err != nil {
return nil, fmt.Errorf("actions: invalid handler %q: %w", def.Name, err)
}
}
if _, exists := names[def.Name]; exists {
return nil, fmt.Errorf("actions: handler %q already registered", def.Name)
}
names[def.Name] = struct{}{}
if def.Key != "" {
if existing, exists := keys[def.Key]; exists {
return nil, fmt.Errorf("actions: key %q already bound to %q", def.Key, existing)
}
keys[def.Key] = def.Name
}
metas = append(metas, actionMeta{name: def.Name, key: def.Key, new: def.New})
}
return metas, nil
}
Parameters
Returns
func (*Router) register(meta actionMeta) error
{
r.mu.Lock()
defer r.mu.Unlock()
if r.container != nil {
instance := meta.new()
if instance == nil {
return fmt.Errorf("actions: constructor returned nil")
}
if err := r.container.ValidateTarget(instance); err != nil {
return fmt.Errorf("actions: invalid handler %q: %w", meta.name, err)
}
}
if _, ok := r.handlers[meta.name]; ok {
return fmt.Errorf("actions: handler %q already registered", meta.name)
}
if _, ok := r.typed[meta.name]; ok {
return fmt.Errorf("actions: handler %q already registered", meta.name)
}
if meta.key != "" {
if existing, ok := r.keys[meta.key]; ok {
return fmt.Errorf("actions: key %q already bound to %q", meta.key, existing)
}
r.keys[meta.key] = meta.name
}
r.handlers[meta.name] = meta
return nil
}
Validate checks every registered declarative action against a container.
Parameters
Returns
func (*Router) Validate(container *di.Container) error
{
r.mu.RLock()
metas := make([]actionMeta, 0, len(r.handlers))
for _, meta := range r.handlers {
metas = append(metas, meta)
}
r.mu.RUnlock()
for _, meta := range metas {
instance := meta.new()
if instance == nil {
return fmt.Errorf("actions: constructor for %q returned nil", meta.name)
}
if err := container.ValidateTarget(instance); err != nil {
return fmt.Errorf("actions: invalid handler %q: %w", meta.name, err)
}
}
return nil
}
Dispatch executes an action by name.
Parameters
Returns
func (*Router) Dispatch(ctx context.Context, name string, payload ...any) (any, error)
{
r.mu.RLock()
meta, ok := r.handlers[name]
typed := r.typed[name]
container := r.container
bus := r.bus
asyncEvents := r.asyncEvents
r.mu.RUnlock()
if !ok && typed == nil {
return nil, fmt.Errorf("actions: no handler for %q", name)
}
if typed != nil {
if len(payload) != 1 {
return nil, fmt.Errorf("actions: typed action %q requires one payload", name)
}
return typed(ctx, payload[0])
}
if container == nil {
var err error
container, err = r.Build()
if err != nil {
return nil, err
}
}
instance := newAction(meta)
if err := container.Inject(instance); err != nil {
return nil, fmt.Errorf("actions: inject %q: %w", name, err)
}
if len(payload) > 0 && payload[0] != nil {
if err := bindPayload(instance, payload[0]); err != nil {
return nil, err
}
}
result, err := instance.Handle(ctx)
eventErr := emitAction(ctx, bus, asyncEvents, instance)
if err != nil {
return result, errors.Join(err, eventErr)
}
return result, eventErr
}
DispatchKey executes an action by key binding.
Parameters
Returns
func (*Router) DispatchKey(ctx context.Context, key string, payload ...any) (any, error)
{
r.mu.RLock()
name, ok := r.keys[key]
r.mu.RUnlock()
if !ok {
return nil, fmt.Errorf("actions: no action bound to %q", key)
}
return r.Dispatch(ctx, name, payload...)
}
Has returns true if an action is registered.
Parameters
Returns
func (*Router) Has(name string) bool
{
r.mu.RLock()
defer r.mu.RUnlock()
_, ok := r.handlers[name]
if ok {
return true
}
_, ok = r.typed[name]
return ok
}
Actions returns registered action names.
Returns
func (*Router) Actions() []string
{
r.mu.RLock()
defer r.mu.RUnlock()
names := make([]string, 0, len(r.handlers))
for name := range r.handlers {
names = append(names, name)
}
for name := range r.typed {
names = append(names, name)
}
sort.Strings(names)
return names
}
KeyBindings returns key-to-action mappings.
Returns
func (*Router) KeyBindings() map[string]string
{
r.mu.RLock()
defer r.mu.RUnlock()
bindings := make(map[string]string, len(r.keys))
for key, name := range r.keys {
bindings[key] = name
}
return bindings
}
Fields
| Name | Type | Description |
|---|---|---|
| mu | sync.RWMutex | |
| handlers | map[string]actionMeta | |
| typed | map[string]func(context.Context, any) (any, error) | |
| keys | map[string]string | |
| container | *di.Container | |
| builder | *di.Builder | |
| bus | *events.Bus | |
| asyncEvents | bool |
Option
Option configures a Router.
type Option options.Option[Router]
New
New creates a Router.
Parameters
Returns
func New(opts ...Option) *Router
{
r := &Router{
handlers: make(map[string]actionMeta),
typed: make(map[string]func(context.Context, any) (any, error)),
keys: make(map[string]string),
builder: di.NewBuilder(),
}
options.Apply(r, opts...)
return r
}
WithContainer
WithContainer uses an existing DI container.
Parameters
Returns
func WithContainer(container *di.Container) Option
{
return func(r *Router) {
r.container = container
}
}
Uses
WithEvents
WithEvents emits the action instance after dispatch.
Parameters
Returns
func WithEvents(bus *events.Bus) Option
{
return func(r *Router) {
r.bus = bus
r.asyncEvents = false
}
}
Uses
WithAsyncEvents
WithAsyncEvents emits the action instance asynchronously after dispatch.
Parameters
Returns
func WithAsyncEvents(bus *events.Bus) Option
{
return func(r *Router) {
r.bus = bus
r.asyncEvents = true
}
}
Uses
parseAction
Parameters
Returns
func parseAction(prototype Handler) actionMeta
{
val := reflect.ValueOf(prototype)
if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
panic("actions: prototype must be a pointer to a struct")
}
typ := val.Elem().Type()
var name, key string
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if action := field.Tag.Get("action"); action != "" {
name = action
}
if keys := field.Tag.Get("keys"); keys != "" {
key = keys
}
}
if name == "" {
panic("actions: struct must have an action tag")
}
return actionMeta{
name: name,
key: key,
new: func() Handler {
newVal := reflect.New(typ).Elem()
newVal.Set(val.Elem())
return newVal.Addr().Interface().(Handler)
},
}
}
HandlerName
HandlerName returns the action name declared by a prototype.
Parameters
Returns
func HandlerName(prototype Handler) (string, error)
{
value := reflect.ValueOf(prototype)
if !value.IsValid() || value.Kind() != reflect.Ptr || value.IsNil() ||
value.Elem().Kind() != reflect.Struct {
return "", fmt.Errorf("actions: prototype must be a non-nil pointer to a struct")
}
typ := value.Elem().Type()
for index := 0; index < typ.NumField(); index++ {
if name := typ.Field(index).Tag.Get("action"); name != "" {
return name, nil
}
}
return "", fmt.Errorf("actions: struct must have an action tag")
}
Uses
newAction
Parameters
Returns
func newAction(meta actionMeta) Handler
{
return meta.new()
}
bindPayload
Parameters
Returns
func bindPayload(target any, payload any) error
{
src := reflect.ValueOf(payload)
if src.Kind() == reflect.Ptr {
if src.IsNil() {
return nil
}
src = src.Elem()
}
switch src.Kind() {
case reflect.Map:
for _, key := range src.MapKeys() {
if err := setPayloadField(target, fmt.Sprint(key.Interface()), src.MapIndex(key)); err != nil {
return err
}
}
case reflect.Struct:
srcType := src.Type()
for i := 0; i < src.NumField(); i++ {
field := srcType.Field(i)
if !field.IsExported() {
continue
}
name := field.Name
if tag := field.Tag.Get("json"); tag != "" {
name = strings.Split(tag, ",")[0]
}
if name == "-" || name == "" {
continue
}
if err := setPayloadField(target, name, src.Field(i)); err != nil {
return err
}
}
default:
return fmt.Errorf("actions: payload must be a map or struct")
}
return nil
}
setPayloadField
Parameters
Returns
func setPayloadField(target any, name string, value reflect.Value) error
{
dst := reflect.ValueOf(target).Elem()
dstType := dst.Type()
nilValue := !value.IsValid() || value.Kind() == reflect.Interface && value.IsNil()
if !nilValue && value.Kind() == reflect.Interface {
value = value.Elem()
}
for i := 0; i < dst.NumField(); i++ {
fieldMeta := dstType.Field(i)
field := dst.Field(i)
if _, injected := fieldMeta.Tag.Lookup("inject"); injected {
continue
}
if !field.CanSet() {
continue
}
if !fieldNameMatches(fieldMeta, name) {
continue
}
if nilValue {
switch field.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Ptr, reflect.Slice:
field.SetZero()
return nil
default:
return fmt.Errorf("actions: cannot bind nil to %s", fieldMeta.Name)
}
}
if value.IsValid() && value.Type().AssignableTo(field.Type()) {
field.Set(value)
return nil
}
if value.IsValid() && value.Type().ConvertibleTo(field.Type()) {
field.Set(value.Convert(field.Type()))
return nil
}
if value.IsValid() && value.Kind() == reflect.String {
return reflectutil.Bind(field, value.String())
}
return fmt.Errorf("actions: cannot bind %s", fieldMeta.Name)
}
return fmt.Errorf("actions: unknown payload field %q", name)
}
fieldNameMatches
Parameters
Returns
func fieldNameMatches(field reflect.StructField, name string) bool
{
if tag := field.Tag.Get("json"); tag != "" {
if strings.EqualFold(strings.Split(tag, ",")[0], name) {
return true
}
}
return false
}
emitAction
Parameters
Returns
func emitAction(ctx context.Context, bus *events.Bus, async bool, event any) error
{
if bus == nil {
return nil
}
if async {
return events.EmitAnyAsync(ctx, bus, event)
}
return events.EmitAny(ctx, bus, event)
}
saveAction
type saveAction struct
Methods
Parameters
Returns
func (*saveAction) Handle(_ context.Context) (any, error)
{
a.Document.saved = a.Name
return a.Count, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | action:"file.save" keys:"ctrl+s" |
| Document | *document | inject:"document" |
| Name | string | json:"name" |
| Count | int | json:"count" |
| IsAdmin | bool |
document
type document struct
Fields
| Name | Type | Description |
|---|---|---|
| saved | string |
duplicateKeyAction
type duplicateKeyAction struct
Methods
Parameters
Returns
func (*duplicateKeyAction) Handle(_ context.Context) (any, error)
{
return nil, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| Meta | struct{} | action:"file.save-as" keys:"ctrl+s" |
TestRouter_Dispatch
Parameters
func TestRouter_Dispatch(t *testing.T)
{
doc := &document{}
r := New()
r.Provide("document", doc)
r.Register(&saveAction{})
result, err := r.Dispatch(context.Background(), "file.save", map[string]any{
"name": "notes.md",
"count": 2,
})
if err != nil {
t.Fatalf("Dispatch() error = %v", err)
}
if result != 2 {
t.Fatalf("result = %v, want 2", result)
}
if doc.saved != "notes.md" {
t.Fatalf("saved = %q, want notes.md", doc.saved)
}
}
TestRouter_DispatchKey
Parameters
func TestRouter_DispatchKey(t *testing.T)
{
doc := &document{}
r := New()
r.Provide("document", doc)
r.Register(&saveAction{})
_, err := r.DispatchKey(context.Background(), "ctrl+s", struct {
Name string `json:"name"`
Count int `json:"count"`
}{Name: "book.md", Count: 1})
if err != nil {
t.Fatalf("DispatchKey() error = %v", err)
}
if doc.saved != "book.md" {
t.Fatalf("saved = %q, want book.md", doc.saved)
}
}
TestRouter_ActionsAndKeyBindings
Parameters
func TestRouter_ActionsAndKeyBindings(t *testing.T)
{
r := New()
r.Register(&saveAction{})
actions := r.Actions()
if len(actions) != 1 || actions[0] != "file.save" {
t.Fatalf("Actions() = %v", actions)
}
bindings := r.KeyBindings()
if bindings["ctrl+s"] != "file.save" {
t.Fatalf("KeyBindings() = %v", bindings)
}
}
TestRouter_DuplicateKeyBindingPanics
Parameters
func TestRouter_DuplicateKeyBindingPanics(t *testing.T)
{
r := New()
r.Register(&saveAction{})
defer func() {
if recover() == nil {
t.Fatal("Register() did not panic")
}
}()
r.Register(&duplicateKeyAction{})
}
TestRouter_Events
Parameters
func TestRouter_Events(t *testing.T)
{
bus := events.New()
defer bus.Close()
var seen *saveAction
events.Subscribe(bus, func(ctx context.Context, e *saveAction) error {
seen = e
return nil
})
r := New(WithEvents(bus))
r.Provide("document", &document{})
r.Register(&saveAction{})
_, err := r.Dispatch(context.Background(), "file.save", map[string]any{"name": "event.md"})
if err != nil {
t.Fatalf("Dispatch() error = %v", err)
}
if seen == nil || seen.Name != "event.md" {
t.Fatalf("seen = %#v", seen)
}
}
TestRouter_AsyncEventsPropagateClosedBus
Parameters
func TestRouter_AsyncEventsPropagateClosedBus(t *testing.T)
{
bus := events.New()
if err := bus.Shutdown(context.Background()); err != nil {
t.Fatalf("Shutdown: %v", err)
}
r := New(WithAsyncEvents(bus))
r.Provide("document", &document{})
r.Register(&saveAction{})
_, err := r.Dispatch(context.Background(), "file.save", map[string]any{"name": "event.md"})
if !errors.Is(err, events.ErrBusClosed) {
t.Fatalf("Dispatch() error = %v, want ErrBusClosed", err)
}
}
TestRouter_MissingAction
Parameters
func TestRouter_MissingAction(t *testing.T)
{
r := New()
if _, err := r.Dispatch(context.Background(), "missing"); err == nil {
t.Fatal("Dispatch() error = nil, want error")
}
}
TestRouter_RejectsUnknownPayloadField
Parameters
func TestRouter_RejectsUnknownPayloadField(t *testing.T)
{
r := New()
r.Provide("document", &document{})
r.Register(&saveAction{})
if _, err := r.Dispatch(context.Background(), "file.save", map[string]any{"typo": "notes.md"}); err == nil {
t.Fatal("Dispatch() accepted an unknown payload field")
}
if _, err := r.Dispatch(context.Background(), "file.save", map[string]any{"typo": nil}); err == nil {
t.Fatal("Dispatch() accepted an unknown nil payload field")
}
}
TestRouter_PayloadCannotOverwriteInjectedDependency
Parameters
func TestRouter_PayloadCannotOverwriteInjectedDependency(t *testing.T)
{
trusted := &document{}
attacker := &document{}
r := New()
r.Provide("document", trusted)
r.Register(&saveAction{})
if _, err := r.Dispatch(context.Background(), "file.save", map[string]any{
"document": attacker,
"name": "trusted.md",
}); err == nil {
t.Fatal("Dispatch() accepted an injected field in the payload")
}
if attacker.saved != "" {
t.Fatalf("payload replaced injected dependency: %#v", attacker)
}
}
TestRouter_PayloadRequiresExplicitFieldTag
Parameters
func TestRouter_PayloadRequiresExplicitFieldTag(t *testing.T)
{
r := New()
r.Provide("document", &document{})
r.Register(&saveAction{})
if _, err := r.Dispatch(context.Background(), "file.save", map[string]any{
"isAdmin": true,
}); err == nil {
t.Fatal("Dispatch() accepted an untagged exported field")
}
}
missingDependencyAction
type missingDependencyAction struct
Methods
Parameters
Returns
func (*missingDependencyAction) Handle(_ context.Context) (any, error)
{
return a.Document, nil
}
Fields
| Name | Type | Description |
|---|---|---|
| _ | struct{} | action:"missing.dependency" |
| Document | *document | inject:"missing" |
TestRouter_RejectsMissingDependency
Parameters
func TestRouter_RejectsMissingDependency(t *testing.T)
{
r := New()
r.Register(&missingDependencyAction{})
if _, err := r.Dispatch(context.Background(), "missing.dependency"); err == nil {
t.Fatal("Dispatch() accepted a missing dependency")
}
}
TestRouter_RegisterDefinition
Parameters
func TestRouter_RegisterDefinition(t *testing.T)
{
r := New()
doc := &document{}
r.Provide("document", doc)
err := r.RegisterDefinition(Definition{
Name: "static.save",
Key: "ctrl+shift+s",
New: func() Handler {
return &saveAction{}
},
})
if err != nil {
t.Fatalf("RegisterDefinition() error = %v", err)
}
result, err := r.Dispatch(context.Background(), "static.save", map[string]any{"name": "static.md"})
if err != nil {
t.Fatalf("Dispatch() error = %v", err)
}
if result != 0 || doc.saved != "static.md" {
t.Fatalf("Dispatch() = %v, saved = %q", result, doc.saved)
}
}
TestRouter_RegisterDefinitionsIsTransactional
Parameters
func TestRouter_RegisterDefinitionsIsTransactional(t *testing.T)
{
router := New()
err := router.RegisterDefinitions(
Definition{Name: "one", Key: "ctrl+x", New: func() Handler { return &saveAction{} }},
Definition{Name: "two", Key: "ctrl+x", New: func() Handler { return &duplicateKeyAction{} }},
)
if err == nil {
t.Fatal("RegisterDefinitions() accepted a duplicate key")
}
if len(router.Actions()) != 0 {
t.Fatalf("RegisterDefinitions() left actions after failure: %v", router.Actions())
}
}
TestRouter_RegisterDefinitionValidatesInjection
Parameters
func TestRouter_RegisterDefinitionValidatesInjection(t *testing.T)
{
router := New(WithContainer(di.NewBuilder().MustBuild()))
err := router.RegisterDefinition(Definition{
Name: "missing.dependency",
New: func() Handler { return &missingDependencyAction{} },
})
if err == nil {
t.Fatal("RegisterDefinition() accepted a missing dependency")
}
}
Typed
Typed identifies an action with compile-time payload and result types.
type Typed struct
Fields
| Name | Type | Description |
|---|---|---|
| name | string |
NewTyped
NewTyped creates a typed action.
Parameters
Returns
func NewTyped[P, R any](name string) Typed[P, R]
{
if name == "" {
panic("actions: action name cannot be empty")
}
return Typed[P, R]{name: name}
}
HandleTyped
HandleTyped registers a typed action handler.
Parameters
Returns
func HandleTyped[P, R any](r *Router, action Typed[P, R], handler func(context.Context, P) (R, error)) error
{
if handler == nil {
return fmt.Errorf("actions: typed handler cannot be nil")
}
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.handlers[action.name]; exists {
return fmt.Errorf("actions: handler %q already registered", action.name)
}
if _, exists := r.typed[action.name]; exists {
return fmt.Errorf("actions: handler %q already registered", action.name)
}
r.typed[action.name] = func(ctx context.Context, payload any) (any, error) {
typed, ok := payload.(P)
if !ok {
return nil, fmt.Errorf("actions: payload for %q has type %T", action.name, payload)
}
return handler(ctx, typed)
}
return nil
}
DispatchTyped
DispatchTyped dispatches an action with compile-time payload and result types.
Parameters
Returns
func DispatchTyped[P, R any](ctx context.Context, r *Router, action Typed[P, R], payload P) (R, error)
{
var zero R
r.mu.RLock()
handler := r.typed[action.name]
r.mu.RUnlock()
if handler == nil {
return zero, fmt.Errorf("actions: no handler for %q", action.name)
}
result, err := handler(ctx, payload)
if err != nil {
return zero, err
}
typed, ok := result.(R)
if !ok {
return zero, fmt.Errorf("actions: result for %q has type %T", action.name, result)
}
return typed, nil
}