From 6f9e3731acf9952ad3f242974c22ead4659d6239 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Tue, 3 Jun 2025 16:58:59 -0700 Subject: [PATCH] add Lifecycle.RequireUnique methods for load-once deps --- lifecycle.go | 23 +++++++++++++++++++ lifecycle_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/lifecycle.go b/lifecycle.go index 9c7de38..e609ced 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -123,6 +123,12 @@ func (app *Lifecycle) Require(modules ...*Module) error { return app.RequireL(app.logger, modules...) } +// RequireUnique is the same as Require, but it returns an error if any requested +// module is already set up--rather than ignoring it. +func (app *Lifecycle) RequireUnique(modules ...*Module) error { + return app.RequireUniqueL(app.logger, modules...) +} + // RequireL adds module(s) to the lifecycle with a specific logger and // immediately runs any setup functions. See Require for more details. // This variation is useful when you need to set up modules with a non- @@ -163,6 +169,23 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { return nil } +// RequireUniqueL is the same as RequireL, but it returns an error if any requested +// module is already set up--rather than ignoring it. +func (app *Lifecycle) RequireUniqueL(logger *slog.Logger, modules ...*Module) error { + // Check if any module is already set up + for _, mod := range modules { + if mod == nil { + return fmt.Errorf("module is nil") + } + + if _, ok := app.setupTracker[mod.name]; ok { + return fmt.Errorf("module '%s' is already set up, cannot require it again", mod.name) + } + } + + return app.RequireL(logger, modules...) +} + // setupSingle is a helper function to set up a single module. Returns an error // if the module cannot be set up or if dependencies are not satisfied. func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error { diff --git a/lifecycle_test.go b/lifecycle_test.go index 0d381e2..ba5b2b3 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -222,6 +222,14 @@ func TestLifecycle_Require(t *testing.T) { assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger") } +func TestLifecycle_RequireUnique(t *testing.T) { + lc := NewLifecycle() + mod := NewModule("module1", ModuleOpts{}) + err := lc.RequireUnique(mod) + assert.NoError(t, err, "expected RequireL to succeed") + assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger") +} + func TestLifecycle_RequireL(t *testing.T) { cases := []struct { name string @@ -324,7 +332,56 @@ func TestLifecycle_RequireL(t *testing.T) { } }) } +} +func TestLifecycle_RequireUniqueL(t *testing.T) { + cases := []struct { + name string + modules []*Module + existingModules []string + expectedErr string + }{ + { + name: "no existing modules", + modules: []*Module{ + NewModule("module1", ModuleOpts{}), + }, + }, + { + name: "module already set up", + modules: []*Module{ + NewModule("module1", ModuleOpts{}), + NewModule("module2", ModuleOpts{}), + }, + existingModules: []string{"module1"}, + expectedErr: "module 'module1' is already set up, cannot require it again", + }, + { + name: "nil module", + modules: []*Module{nil}, + expectedErr: "module is nil", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert := assert.New(t) + lc := NewLifecycle() + + // Fake existing modules + for _, modName := range tc.existingModules { + lc.setupTracker[modName] = 0 // Mark as set up + } + + err := lc.RequireUniqueL(lc.logger, tc.modules...) + if tc.expectedErr == "" { + assert.NoError(err, "expected RequireUniqueL to succeed") + } else { + assert.Error(err, "expected RequireUniqueL to fail") + assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") + } + }) + } } func TestLifecycle_setupSingle(t *testing.T) {