actions API

actions

package

API reference for the actions package.

S
struct

renameInput

app/actions/typed_test.go:8-10
type renameInput struct

Fields

Name Type Description
Name string
S
struct

renameResult

app/actions/typed_test.go:12-14
type renameResult struct

Fields

Name Type Description
Path string
F
function

TestTypedAction

Parameters

app/actions/typed_test.go:16-36
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")
	}
}
F
function

TestTypedActionRejectsRuntimeTypeMismatch

Parameters

app/actions/typed_test.go:38-50
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")
	}
}
I
interface

Handler

Handler is the interface for declarative actions.

app/actions/actions.go:19-21
type Handler interface

Methods

Handle
Method

Parameters

Returns

any
error
func Handle(...)
S
struct

Definition

Definition describes an action without runtime metadata discovery.

app/actions/actions.go:24-28
type Definition struct

Fields

Name Type Description
Name string
Key string
New func() Handler
S
struct

actionMeta

app/actions/actions.go:30-34
type actionMeta struct

Fields

Name Type Description
name string
key string
new func() Handler
S
struct

Router

Router dispatches named actions and optional key bindings.

app/actions/actions.go:37-46
type Router struct

Methods

UseContainer
Method

UseContainer replaces the DI container used during dispatch.

Parameters

container *di.Container
func (*Router) UseContainer(container *di.Container)
{
	r.mu.Lock()
	r.container = container
	r.mu.Unlock()
}
UseEvents
Method

UseEvents emits the action instance after dispatch.

Parameters

bus *events.Bus
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

bus *events.Bus
func (*Router) UseAsyncEvents(bus *events.Bus)
{
	r.mu.Lock()
	r.bus = bus
	r.asyncEvents = true
	r.mu.Unlock()
}
Provide
Method

Provide registers a named dependency for action handlers.

Parameters

name string
instance any
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
Method

Build constructs the DI container used during dispatch.

Returns

error
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
Method

Register registers an action handler.

Parameters

prototype Handler
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

error
func (*Router) RegisterDefinition(def Definition) error
{
	return r.RegisterDefinitions(def)
}

RegisterDefinitions validates and registers static actions as one operation.

Parameters

defs ...Definition

Returns

error
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

container *di.Container
defs ...Definition

Returns

error
func (*Router) ValidateDefinitions(container *di.Container, defs ...Definition) error
{
	r.mu.RLock()
	defer r.mu.RUnlock()
	_, err := r.validateDefinitionsLocked(container, defs)
	return err
}

Parameters

container *di.Container
defs []Definition

Returns

error
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
}
register
Method

Parameters

meta actionMeta

Returns

error
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
Method

Validate checks every registered declarative action against a container.

Parameters

container *di.Container

Returns

error
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
Method

Dispatch executes an action by name.

Parameters

name string
payload ...any

Returns

any
error
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
Method

DispatchKey executes an action by key binding.

Parameters

key string
payload ...any

Returns

any
error
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
Method

Has returns true if an action is registered.

Parameters

name string

Returns

bool
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
Method

Actions returns registered action names.

Returns

[]string
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
Method

KeyBindings returns key-to-action mappings.

Returns

map[string]string
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
T
type

Option

Option configures a Router.

app/actions/actions.go:49-49
type Option options.Option[Router]
F
function

New

New creates a Router.

Parameters

opts
...Option

Returns

app/actions/actions.go:52-61
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
}
F
function

WithContainer

WithContainer uses an existing DI container.

Parameters

container

Returns

app/actions/actions.go:64-68
func WithContainer(container *di.Container) Option

{
	return func(r *Router) {
		r.container = container
	}
}
F
function

WithEvents

WithEvents emits the action instance after dispatch.

Parameters

bus

Returns

app/actions/actions.go:71-76
func WithEvents(bus *events.Bus) Option

{
	return func(r *Router) {
		r.bus = bus
		r.asyncEvents = false
	}
}
F
function

WithAsyncEvents

WithAsyncEvents emits the action instance asynchronously after dispatch.

Parameters

bus

Returns

app/actions/actions.go:79-84
func WithAsyncEvents(bus *events.Bus) Option

{
	return func(r *Router) {
		r.bus = bus
		r.asyncEvents = true
	}
}
F
function

parseAction

Parameters

prototype

Returns

app/actions/actions.go:374-404
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)
		},
	}
}
F
function

HandlerName

HandlerName returns the action name declared by a prototype.

Parameters

prototype

Returns

string
error
app/actions/actions.go:407-420
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")
}
F
function

newAction

Parameters

meta

Returns

app/actions/actions.go:422-424
func newAction(meta actionMeta) Handler

{
	return meta.new()
}
F
function

bindPayload

Parameters

target
any
payload
any

Returns

error
app/actions/actions.go:426-464
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
}
F
function

setPayloadField

Parameters

target
any
name
string

Returns

error
app/actions/actions.go:466-511
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)
}
F
function

fieldNameMatches

Parameters

name
string

Returns

bool
app/actions/actions.go:513-520
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
}
F
function

emitAction

Parameters

bus
async
bool
event
any

Returns

error
app/actions/actions.go:522-530
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)
}
S
struct

saveAction

app/actions/actions_test.go:14-20
type saveAction struct

Methods

Handle
Method

Parameters

Returns

any
error
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
S
struct

document

app/actions/actions_test.go:22-24
type document struct

Fields

Name Type Description
saved string
S
struct

duplicateKeyAction

app/actions/actions_test.go:31-33
type duplicateKeyAction struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*duplicateKeyAction) Handle(_ context.Context) (any, error)
{
	return nil, nil
}

Fields

Name Type Description
Meta struct{} action:"file.save-as" keys:"ctrl+s"
F
function

TestRouter_Dispatch

Parameters

app/actions/actions_test.go:39-58
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)
	}
}
F
function

TestRouter_DispatchKey

Parameters

app/actions/actions_test.go:60-76
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)
	}
}
F
function

TestRouter_ActionsAndKeyBindings

Parameters

app/actions/actions_test.go:78-91
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)
	}
}
F
function

TestRouter_DuplicateKeyBindingPanics

Parameters

app/actions/actions_test.go:93-104
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{})
}
F
function

TestRouter_Events

Parameters

app/actions/actions_test.go:106-127
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)
	}
}
F
function

TestRouter_AsyncEventsPropagateClosedBus

Parameters

app/actions/actions_test.go:129-143
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)
	}
}
F
function

TestRouter_MissingAction

Parameters

app/actions/actions_test.go:145-151
func TestRouter_MissingAction(t *testing.T)

{
	r := New()

	if _, err := r.Dispatch(context.Background(), "missing"); err == nil {
		t.Fatal("Dispatch() error = nil, want error")
	}
}
F
function

TestRouter_RejectsUnknownPayloadField

Parameters

app/actions/actions_test.go:153-164
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")
	}
}
F
function

TestRouter_PayloadCannotOverwriteInjectedDependency

Parameters

app/actions/actions_test.go:166-182
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)
	}
}
F
function

TestRouter_PayloadRequiresExplicitFieldTag

Parameters

app/actions/actions_test.go:184-194
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")
	}
}
S
struct

missingDependencyAction

app/actions/actions_test.go:196-199
type missingDependencyAction struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*missingDependencyAction) Handle(_ context.Context) (any, error)
{
	return a.Document, nil
}

Fields

Name Type Description
_ struct{} action:"missing.dependency"
Document *document inject:"missing"
F
function

TestRouter_RejectsMissingDependency

Parameters

app/actions/actions_test.go:205-212
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")
	}
}
F
function

TestRouter_RegisterDefinition

Parameters

app/actions/actions_test.go:214-236
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)
	}
}
F
function

TestRouter_RegisterDefinitionsIsTransactional

Parameters

app/actions/actions_test.go:238-250
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())
	}
}
F
function

TestRouter_RegisterDefinitionValidatesInjection

Parameters

app/actions/actions_test.go:252-261
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")
	}
}
S
struct

Typed

Typed identifies an action with compile-time payload and result types.

app/actions/typed.go:9-11
type Typed struct

Fields

Name Type Description
name string
F
function

NewTyped

NewTyped creates a typed action.

Parameters

name
string

Returns

Typed[P,
R]
app/actions/typed.go:14-19
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}
}
F
function

HandleTyped

HandleTyped registers a typed action handler.

Parameters

r
action
Typed[P, R]
handler
func(context.Context, P) (R, error)

Returns

error
app/actions/typed.go:27-48
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
}
F
function

DispatchTyped

DispatchTyped dispatches an action with compile-time payload and result types.

Parameters

r
action
Typed[P, R]
payload
P

Returns

R
error
app/actions/typed.go:51-69
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
}