diff --git a/pkg/core/contract.go b/pkg/core/contract.go index 3914c0a..2e99060 100644 --- a/pkg/core/contract.go +++ b/pkg/core/contract.go @@ -24,11 +24,11 @@ type TaskWithID interface { GetTaskID() string } -// QueryHandler handles Query requests. Returns (result, handled, error). -type QueryHandler func(*Core, Query) (any, bool, error) +// QueryHandler handles Query requests. Returns Result{Value, OK}. +type QueryHandler func(*Core, Query) Result -// TaskHandler handles Task requests. Returns (result, handled, error). -type TaskHandler func(*Core, Task) (any, bool, error) +// TaskHandler handles Task requests. Returns Result{Value, OK}. +type TaskHandler func(*Core, Task) Result // Startable is implemented by services that need startup initialisation. type Startable interface { diff --git a/pkg/core/core.go b/pkg/core/core.go index 40d9fc4..7f78749 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -52,21 +52,29 @@ func (c *Core) Core() *Core { return c } // --- IPC (uppercase aliases) --- -func (c *Core) ACTION(msg Message) error { return c.Action(msg) } -func (c *Core) QUERY(q Query) (any, bool, error) { return c.Query(q) } -func (c *Core) QUERYALL(q Query) ([]any, error) { return c.QueryAll(q) } -func (c *Core) PERFORM(t Task) (any, bool, error) { return c.Perform(t) } +func (c *Core) ACTION(msg Message) Result { return c.Action(msg) } +func (c *Core) QUERY(q Query) Result { return c.Query(q) } +func (c *Core) QUERYALL(q Query) Result { return c.QueryAll(q) } +func (c *Core) PERFORM(t Task) Result { return c.Perform(t) } // --- Error+Log --- -// LogError logs an error and returns a wrapped error. -func (c *Core) LogError(err error, op, msg string) error { - return c.log.Error(err, op, msg) +// LogError logs an error and returns a Result with the wrapped error. +func (c *Core) LogError(err error, op, msg string) Result { + wrapped := c.log.Error(err, op, msg) + if wrapped == nil { + return Result{OK: true} + } + return Result{Value: wrapped} } -// LogWarn logs a warning and returns a wrapped error. -func (c *Core) LogWarn(err error, op, msg string) error { - return c.log.Warn(err, op, msg) +// LogWarn logs a warning and returns a Result with the wrapped error. +func (c *Core) LogWarn(err error, op, msg string) Result { + wrapped := c.log.Warn(err, op, msg) + if wrapped == nil { + return Result{OK: true} + } + return Result{Value: wrapped} } // Must logs and panics if err is not nil. diff --git a/pkg/core/ipc.go b/pkg/core/ipc.go index aa66d0e..9a4d958 100644 --- a/pkg/core/ipc.go +++ b/pkg/core/ipc.go @@ -7,7 +7,6 @@ package core import ( - "errors" "slices" "sync" ) @@ -15,7 +14,7 @@ import ( // Ipc holds IPC dispatch data. type Ipc struct { ipcMu sync.RWMutex - ipcHandlers []func(*Core, Message) error + ipcHandlers []func(*Core, Message) Result queryMu sync.RWMutex queryHandlers []QueryHandler @@ -24,51 +23,46 @@ type Ipc struct { taskHandlers []TaskHandler } -func (c *Core) Action(msg Message) error { +func (c *Core) Action(msg Message) Result { c.ipc.ipcMu.RLock() handlers := slices.Clone(c.ipc.ipcHandlers) c.ipc.ipcMu.RUnlock() - var agg error for _, h := range handlers { - if err := h(c, msg); err != nil { - agg = errors.Join(agg, err) + if r := h(c, msg); !r.OK { + return r } } - return agg + return Result{OK: true} } -func (c *Core) Query(q Query) (any, bool, error) { +func (c *Core) Query(q Query) Result { c.ipc.queryMu.RLock() handlers := slices.Clone(c.ipc.queryHandlers) c.ipc.queryMu.RUnlock() for _, h := range handlers { - result, handled, err := h(c, q) - if handled { - return result, true, err + r := h(c, q) + if r.OK { + return r } } - return nil, false, nil + return Result{} } -func (c *Core) QueryAll(q Query) ([]any, error) { +func (c *Core) QueryAll(q Query) Result { c.ipc.queryMu.RLock() handlers := slices.Clone(c.ipc.queryHandlers) c.ipc.queryMu.RUnlock() var results []any - var agg error for _, h := range handlers { - result, handled, err := h(c, q) - if err != nil { - agg = errors.Join(agg, err) - } - if handled && result != nil { - results = append(results, result) + r := h(c, q) + if r.OK && r.Value != nil { + results = append(results, r.Value) } } - return results, agg + return Result{Value: results, OK: true} } func (c *Core) RegisterQuery(handler QueryHandler) { diff --git a/pkg/core/runtime.go b/pkg/core/runtime.go index 16334d8..5e2aebb 100644 --- a/pkg/core/runtime.go +++ b/pkg/core/runtime.go @@ -32,29 +32,27 @@ func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() } // --- Lifecycle --- // ServiceStartup runs OnStart for all registered services that have one. -func (c *Core) ServiceStartup(ctx context.Context, options any) error { +func (c *Core) ServiceStartup(ctx context.Context, options any) Result { for _, s := range c.Startables() { if err := ctx.Err(); err != nil { - return err + return Result{Value: err} } r := s.OnStart() if !r.OK { - if err, ok := r.Value.(error); ok { - return err - } + return r } } - _ = c.ACTION(ActionServiceStartup{}) - return nil + c.ACTION(ActionServiceStartup{}) + return Result{OK: true} } // ServiceShutdown runs OnStop for all registered services that have one. -func (c *Core) ServiceShutdown(ctx context.Context) error { +func (c *Core) ServiceShutdown(ctx context.Context) Result { c.shutdown.Store(true) - _ = c.ACTION(ActionServiceShutdown{}) + c.ACTION(ActionServiceShutdown{}) for _, s := range c.Stoppables() { if err := ctx.Err(); err != nil { - return err + return Result{Value: err} } s.OnStop() } @@ -66,9 +64,9 @@ func (c *Core) ServiceShutdown(ctx context.Context) error { select { case <-done: case <-ctx.Done(): - return ctx.Err() + return Result{Value: ctx.Err()} } - return nil + return Result{OK: true} } // --- Runtime DTO (GUI binding) --- @@ -110,12 +108,12 @@ func NewRuntime(app any) Result { } func (r *Runtime) ServiceName() string { return "Core" } -func (r *Runtime) ServiceStartup(ctx context.Context, options any) error { +func (r *Runtime) ServiceStartup(ctx context.Context, options any) Result { return r.Core.ServiceStartup(ctx, options) } -func (r *Runtime) ServiceShutdown(ctx context.Context) error { +func (r *Runtime) ServiceShutdown(ctx context.Context) Result { if r.Core != nil { return r.Core.ServiceShutdown(ctx) } - return nil + return Result{OK: true} } diff --git a/pkg/core/task.go b/pkg/core/task.go index 94488c5..7ad1a40 100644 --- a/pkg/core/task.go +++ b/pkg/core/task.go @@ -27,43 +27,48 @@ func (c *Core) PerformAsync(t Task) string { if tid, ok := t.(TaskWithID); ok { tid.SetTaskID(taskID) } - _ = c.ACTION(ActionTaskStarted{TaskID: taskID, Task: t}) + c.ACTION(ActionTaskStarted{TaskID: taskID, Task: t}) c.wg.Go(func() { - result, handled, err := c.PERFORM(t) - if !handled && err == nil { - err = E("core.PerformAsync", Join(" ", "no handler found for task type", reflect.TypeOf(t).String()), nil) + r := c.PERFORM(t) + var err error + if !r.OK { + if e, ok := r.Value.(error); ok { + err = e + } else { + err = E("core.PerformAsync", Join(" ", "no handler found for task type", reflect.TypeOf(t).String()), nil) + } } - _ = c.ACTION(ActionTaskCompleted{TaskID: taskID, Task: t, Result: result, Error: err}) + c.ACTION(ActionTaskCompleted{TaskID: taskID, Task: t, Result: r.Value, Error: err}) }) return taskID } // Progress broadcasts a progress update for a background task. func (c *Core) Progress(taskID string, progress float64, message string, t Task) { - _ = c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message}) + c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message}) } -func (c *Core) Perform(t Task) (any, bool, error) { +func (c *Core) Perform(t Task) Result { c.ipc.taskMu.RLock() handlers := slices.Clone(c.ipc.taskHandlers) c.ipc.taskMu.RUnlock() for _, h := range handlers { - result, handled, err := h(c, t) - if handled { - return result, true, err + r := h(c, t) + if r.OK { + return r } } - return nil, false, nil + return Result{} } -func (c *Core) RegisterAction(handler func(*Core, Message) error) { +func (c *Core) RegisterAction(handler func(*Core, Message) Result) { c.ipc.ipcMu.Lock() c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handler) c.ipc.ipcMu.Unlock() } -func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) { +func (c *Core) RegisterActions(handlers ...func(*Core, Message) Result) { c.ipc.ipcMu.Lock() c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handlers...) c.ipc.ipcMu.Unlock() diff --git a/tests/core_test.go b/tests/core_test.go index d69c78c..1a8ab17 100644 --- a/tests/core_test.go +++ b/tests/core_test.go @@ -67,15 +67,19 @@ func TestOptions_Accessor_Nil(t *testing.T) { func TestCore_LogError_Good(t *testing.T) { c := New() cause := assert.AnError - err := c.LogError(cause, "test.Op", "something broke") - assert.Error(t, err) + r := c.LogError(cause, "test.Op", "something broke") + assert.False(t, r.OK) + err, ok := r.Value.(error) + assert.True(t, ok) assert.ErrorIs(t, err, cause) } func TestCore_LogWarn_Good(t *testing.T) { c := New() - err := c.LogWarn(assert.AnError, "test.Op", "heads up") - assert.Error(t, err) + r := c.LogWarn(assert.AnError, "test.Op", "heads up") + assert.False(t, r.OK) + _, ok := r.Value.(error) + assert.True(t, ok) } func TestCore_Must_Ugly(t *testing.T) { diff --git a/tests/ipc_test.go b/tests/ipc_test.go index fc72ff1..44f1fdc 100644 --- a/tests/ipc_test.go +++ b/tests/ipc_test.go @@ -14,67 +14,66 @@ type testMessage struct{ payload string } func TestAction_Good(t *testing.T) { c := New() var received Message - c.RegisterAction(func(_ *Core, msg Message) error { + c.RegisterAction(func(_ *Core, msg Message) Result { received = msg - return nil + return Result{OK: true} }) - err := c.ACTION(testMessage{payload: "hello"}) - assert.NoError(t, err) + r := c.ACTION(testMessage{payload: "hello"}) + assert.True(t, r.OK) assert.Equal(t, testMessage{payload: "hello"}, received) } func TestAction_Multiple_Good(t *testing.T) { c := New() count := 0 - handler := func(_ *Core, _ Message) error { count++; return nil } + handler := func(_ *Core, _ Message) Result { count++; return Result{OK: true} } c.RegisterActions(handler, handler, handler) - _ = c.ACTION(nil) + c.ACTION(nil) assert.Equal(t, 3, count) } func TestAction_None_Good(t *testing.T) { c := New() - // No handlers registered — should not error - err := c.ACTION(nil) - assert.NoError(t, err) + // No handlers registered — should succeed + r := c.ACTION(nil) + assert.True(t, r.OK) } // --- IPC: Queries --- func TestQuery_Good(t *testing.T) { c := New() - c.RegisterQuery(func(_ *Core, q Query) (any, bool, error) { + c.RegisterQuery(func(_ *Core, q Query) Result { if q == "ping" { - return "pong", true, nil + return Result{Value: "pong", OK: true} } - return nil, false, nil + return Result{} }) - result, handled, err := c.QUERY("ping") - assert.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, "pong", result) + r := c.QUERY("ping") + assert.True(t, r.OK) + assert.Equal(t, "pong", r.Value) } func TestQuery_Unhandled_Good(t *testing.T) { c := New() - c.RegisterQuery(func(_ *Core, q Query) (any, bool, error) { - return nil, false, nil + c.RegisterQuery(func(_ *Core, q Query) Result { + return Result{} }) - _, handled, err := c.QUERY("unknown") - assert.NoError(t, err) - assert.False(t, handled) + r := c.QUERY("unknown") + assert.False(t, r.OK) } func TestQueryAll_Good(t *testing.T) { c := New() - c.RegisterQuery(func(_ *Core, _ Query) (any, bool, error) { - return "a", true, nil + c.RegisterQuery(func(_ *Core, _ Query) Result { + return Result{Value: "a", OK: true} }) - c.RegisterQuery(func(_ *Core, _ Query) (any, bool, error) { - return "b", true, nil + c.RegisterQuery(func(_ *Core, _ Query) Result { + return Result{Value: "b", OK: true} }) - results, err := c.QUERYALL("anything") - assert.NoError(t, err) + r := c.QUERYALL("anything") + assert.True(t, r.OK) + results := r.Value.([]any) assert.Len(t, results, 2) assert.Contains(t, results, "a") assert.Contains(t, results, "b") @@ -84,14 +83,13 @@ func TestQueryAll_Good(t *testing.T) { func TestPerform_Good(t *testing.T) { c := New() - c.RegisterTask(func(_ *Core, t Task) (any, bool, error) { + c.RegisterTask(func(_ *Core, t Task) Result { if t == "compute" { - return 42, true, nil + return Result{Value: 42, OK: true} } - return nil, false, nil + return Result{} }) - result, handled, err := c.PERFORM("compute") - assert.NoError(t, err) - assert.True(t, handled) - assert.Equal(t, 42, result) + r := c.PERFORM("compute") + assert.True(t, r.OK) + assert.Equal(t, 42, r.Value) } diff --git a/tests/runtime_test.go b/tests/runtime_test.go index 5680fc0..95ada1d 100644 --- a/tests/runtime_test.go +++ b/tests/runtime_test.go @@ -70,7 +70,7 @@ func TestRuntime_Lifecycle_Good(t *testing.T) { assert.True(t, r.OK) rt := r.Value.(*Runtime) - err := rt.ServiceStartup(context.Background(), nil) - assert.NoError(t, err) + result := rt.ServiceStartup(context.Background(), nil) + assert.True(t, result.OK) assert.True(t, started) } diff --git a/tests/task_test.go b/tests/task_test.go index 590279a..df1071d 100644 --- a/tests/task_test.go +++ b/tests/task_test.go @@ -16,11 +16,11 @@ func TestPerformAsync_Good(t *testing.T) { var mu sync.Mutex var result string - c.RegisterTask(func(_ *Core, task Task) (any, bool, error) { + c.RegisterTask(func(_ *Core, task Task) Result { mu.Lock() result = "done" mu.Unlock() - return "completed", true, nil + return Result{Value: "completed", OK: true} }) taskID := c.PerformAsync("work") @@ -35,8 +35,8 @@ func TestPerformAsync_Good(t *testing.T) { func TestPerformAsync_Progress_Good(t *testing.T) { c := New() - c.RegisterTask(func(_ *Core, task Task) (any, bool, error) { - return nil, true, nil + c.RegisterTask(func(_ *Core, task Task) Result { + return Result{OK: true} }) taskID := c.PerformAsync("work") @@ -48,19 +48,19 @@ func TestPerformAsync_Progress_Good(t *testing.T) { func TestRegisterAction_Good(t *testing.T) { c := New() called := false - c.RegisterAction(func(_ *Core, _ Message) error { + c.RegisterAction(func(_ *Core, _ Message) Result { called = true - return nil + return Result{OK: true} }) - _ = c.Action(nil) + c.Action(nil) assert.True(t, called) } func TestRegisterActions_Good(t *testing.T) { c := New() count := 0 - h := func(_ *Core, _ Message) error { count++; return nil } + h := func(_ *Core, _ Message) Result { count++; return Result{OK: true} } c.RegisterActions(h, h) - _ = c.Action(nil) + c.Action(nil) assert.Equal(t, 2, count) }