context helpers, rework logging to support ongoing inheritance

Leaves module logger property as purely an override, pulling logger
information directly from the lifecycle instead.
This commit is contained in:
Elijah Duffy
2025-06-04 13:36:40 -07:00
parent 6f9e3731ac
commit c3c8a2cafc
5 changed files with 188 additions and 115 deletions

8
go.mod
View File

@@ -1,3 +1,11 @@
module gitea.auvem.com/go-toolkit/app module gitea.auvem.com/go-toolkit/app
go 1.24.0 go 1.24.0
require github.com/stretchr/testify v1.10.0
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum Normal file
View File

@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,6 +1,7 @@
package app package app
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -8,19 +9,23 @@ import (
"strings" "strings"
) )
type contextKey string
const lifecycleContextKey contextKey = "lifecycle"
// LifecycleOpts contains user-exposed options when defining a lifecycle. // LifecycleOpts contains user-exposed options when defining a lifecycle.
type LifecycleOpts struct { type LifecycleOpts struct {
// DisableAutoload disables dependency autoloading (enabled by default). // DisableAutoload disables dependency autoloading (enabled by default).
DisableAutoload bool DisableAutoload bool
// Logger is the logger for the lifecycle. If not set, it will be initialized
// by the lifecycle using the default logger (slog.Default).
Logger *slog.Logger
} }
// Lifecycle represents the core application structure. Lifecycle manages // Lifecycle represents the core application structure. Lifecycle manages
// resources providing an orchestrator for setup and teardown of modules. // resources providing an orchestrator for setup and teardown of modules.
type Lifecycle struct { type Lifecycle struct {
// Logger is the logger for the lifecycle. If not set, it will be initialized
// by the lifecycle itself using the default logger.
logger *slog.Logger
modules []*Module modules []*Module
opts LifecycleOpts opts LifecycleOpts
@@ -33,46 +38,59 @@ type Lifecycle struct {
// NewLifecycle creates a new Lifecycle instance with a default logger and the // NewLifecycle creates a new Lifecycle instance with a default logger and the
// given modules. It panics if any module has a duplicate name. // given modules. It panics if any module has a duplicate name.
func NewLifecycle(modules ...*Module) *Lifecycle { func NewLifecycle(modules ...*Module) *Lifecycle {
return NewLifecycleL(nil, modules...)
}
// NewLifecycleL creates a new Lifecycle instance with a specific logger and the
// given modules. See NewLifecycle for more details.
func NewLifecycleL(defaultLogger *slog.Logger, modules ...*Module) *Lifecycle {
// Use the provided logger or initialize a default one
if defaultLogger == nil {
defaultLogger = slog.Default()
}
// Ensure modules are unique // Ensure modules are unique
unique := make(map[string]bool) unique := make(map[string]bool)
for _, mod := range modules { for _, mod := range modules {
if _, exists := unique[mod.name]; exists { if _, exists := unique[mod.name]; exists {
panic(fmt.Sprintf("duplicate module name: %s", mod.name)) panic(fmt.Sprintf("duplicate module: %s", mod))
} }
unique[mod.name] = true unique[mod.name] = true
} }
return &Lifecycle{ return &Lifecycle{
logger: defaultLogger,
modules: modules, modules: modules,
setupTracker: make(map[string]int), setupTracker: make(map[string]int),
teardownTracker: make(map[string]int), teardownTracker: make(map[string]int),
} }
} }
// LifecycleToContext adds the Lifecycle to the context.
func LifecycleToContext(ctx context.Context, lifecycle *Lifecycle) context.Context {
if lifecycle == nil {
return ctx
}
return context.WithValue(ctx, lifecycleContextKey, lifecycle)
}
// LifecycleFromContext retrieves the Lifecycle from the context. Returns nil if not found.
func LifecycleFromContext(ctx context.Context) *Lifecycle {
if lifecycle, ok := ctx.Value(lifecycleContextKey).(*Lifecycle); ok {
return lifecycle
}
return nil
}
// WithOpts sets the options for the lifecycle. // WithOpts sets the options for the lifecycle.
func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle { func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle {
app.opts = opts app.opts = opts
return app return app
} }
// WithLogger sets the logger for the lifecycle. Panics if the logger is nil.
func (app *Lifecycle) WithLogger(logger *slog.Logger) *Lifecycle {
if logger == nil {
panic("logger cannot be nil")
}
app.opts.Logger = logger
return app
}
// Logger returns the logger for the lifecycle. // Logger returns the logger for the lifecycle.
func (app *Lifecycle) Logger() *slog.Logger { func (app *Lifecycle) Logger() *slog.Logger {
if app.logger == nil { if app.opts.Logger == nil {
panic("lifecycle logger is not initialized") app.opts.Logger = slog.Default()
} }
return app.logger return app.opts.Logger
} }
// Setup initializes all modules in the order they were defined and checks // Setup initializes all modules in the order they were defined and checks
@@ -81,12 +99,12 @@ func (app *Lifecycle) Logger() *slog.Logger {
// of the application lifecycle to ensure all resources are cleaned up properly. // of the application lifecycle to ensure all resources are cleaned up properly.
func (app *Lifecycle) Setup() error { func (app *Lifecycle) Setup() error {
for _, mod := range app.modules { for _, mod := range app.modules {
if err := app.setupSingle(mod, app.logger); err != nil { if err := app.setupSingle(nil, mod); err != nil {
return err return err
} }
} }
app.logger.Info("Lifecycle modules initialized, backend setup complete", app.Logger().Info("Lifecycle modules initialized, backend setup complete",
"modules", mapToString(app.setupTracker)) "modules", mapToString(app.setupTracker))
return nil return nil
@@ -105,11 +123,11 @@ func (app *Lifecycle) Teardown() error {
} }
if err != nil { if err != nil {
app.logger.Error("Error tearing down modules", "failures", app.setupCount-app.teardownCount, "error", err) app.Logger().Error("Error tearing down modules", "failures", app.setupCount-app.teardownCount, "error", err)
return err return err
} }
app.logger.Info("All modules torn down, backend teardown complete", app.Logger().Info("All modules torn down, backend teardown complete",
"modules", mapToString(app.teardownTracker)) "modules", mapToString(app.teardownTracker))
return nil return nil
@@ -120,13 +138,13 @@ func (app *Lifecycle) Teardown() error {
// but may still be conditionally necessary. Any modules that are already // but may still be conditionally necessary. Any modules that are already
// set up are ignored. // set up are ignored.
func (app *Lifecycle) Require(modules ...*Module) error { func (app *Lifecycle) Require(modules ...*Module) error {
return app.RequireL(app.logger, modules...) return app.require(nil, false, modules...)
} }
// RequireUnique is the same as Require, but it returns an error if any requested // RequireUnique is the same as Require, but it returns an error if any requested
// module is already set up--rather than ignoring it. // module is already set up--rather than ignoring it.
func (app *Lifecycle) RequireUnique(modules ...*Module) error { func (app *Lifecycle) RequireUnique(modules ...*Module) error {
return app.RequireUniqueL(app.logger, modules...) return app.require(nil, true, modules...)
} }
// RequireL adds module(s) to the lifecycle with a specific logger and // RequireL adds module(s) to the lifecycle with a specific logger and
@@ -135,21 +153,39 @@ func (app *Lifecycle) RequireUnique(modules ...*Module) error {
// default logger. // default logger.
func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
if logger == nil { if logger == nil {
logger = app.logger return fmt.Errorf("logger cannot be nil")
}
return app.require(logger, false, modules...)
} }
// 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 {
if logger == nil {
return fmt.Errorf("logger cannot be nil")
}
return app.require(logger, true, modules...)
}
// require is a helper function that attempts to add module(s) to the lifecycle
// and immediately run any setup functions.
func (app *Lifecycle) require(logger *slog.Logger, unique bool, modules ...*Module) error {
if len(modules) == 0 { if len(modules) == 0 {
return fmt.Errorf("no modules to require") return fmt.Errorf("no modules to require")
} }
for _, mod := range modules { for i, mod := range modules {
if mod == nil { if mod == nil {
return fmt.Errorf("module is nil") return fmt.Errorf("module %d is nil", i)
} }
// Check if the module is already set up // Check if the module has already been set up
if _, ok := app.setupTracker[mod.name]; ok { if _, ok := app.setupTracker[mod.name]; ok {
app.logger.Warn("module already set up, ignoring", "module", mod.name) if unique {
return fmt.Errorf("module %s is already set up, cannot require it again", mod)
}
app.Logger().Warn("module already set up, ignoring", "module", mod)
mod.loaded = true mod.loaded = true
mod.logger = logger mod.logger = logger
continue continue
@@ -159,57 +195,40 @@ func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error {
app.modules = append(app.modules, mod) app.modules = append(app.modules, mod)
// Run the setup function for the module // Run the setup function for the module
if err := app.setupSingle(mod, logger); err != nil { if err := app.setupSingle(logger, mod); err != nil {
return fmt.Errorf("error setting up required module '%s': %w", mod.name, err) return fmt.Errorf("error setting up required module %s: %w", mod, err)
} }
} }
app.logger.Info("New modules initialized", "all", mapToString(app.setupTracker)) app.Logger().Info("New modules initialized", "all", mapToString(app.setupTracker))
return nil 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 // 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. // if the module cannot be set up or if dependencies are not satisfied.
func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error { func (app *Lifecycle) setupSingle(logger *slog.Logger, mod *Module) error {
if mod == nil { if mod == nil {
return fmt.Errorf("module is nil") return fmt.Errorf("module is nil")
} }
// Set the logger for the module // Set the parent lifecycle and logger override
mod.lifecycle = app
mod.logger = logger mod.logger = logger
// Check if all dependencies are satisfied // Check if all dependencies are satisfied
for _, dep := range mod.depends { for _, dep := range mod.depends {
if _, ok := app.setupTracker[dep]; !ok { if _, ok := app.setupTracker[dep]; !ok {
if app.opts.DisableAutoload { if app.opts.DisableAutoload {
return fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name) return fmt.Errorf("dependency %s not satisfied for '%s'", dep, mod)
} else { } else {
// Attempt to set up the dependency // Attempt to set up the dependency
depmod, err := app.getModuleByName(dep) depmod, err := app.getModuleByName(dep)
if err != nil { if err != nil {
return fmt.Errorf("error getting dependency '%s' for '%s': %w", dep, mod.name, err) return fmt.Errorf("error getting dependency '%s' for %s: %w", dep, mod, err)
} }
if err := app.setupSingle(depmod, logger); err != nil { if err := app.setupSingle(logger, depmod); err != nil {
return fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err) return fmt.Errorf("error setting up dependency %s for %s: %w", depmod, mod, err)
} }
} }
} }
@@ -218,7 +237,7 @@ func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) error {
if mod.setup != nil { if mod.setup != nil {
// Run the setup function for the module // Run the setup function for the module
if err := mod.setup(mod); err != nil { if err := mod.setup(mod); err != nil {
return fmt.Errorf("error initializing '%s': %w", mod.name, err) return fmt.Errorf("error initializing %s: %w", mod, err)
} }
} }
@@ -238,13 +257,13 @@ func (app *Lifecycle) teardownSingle(mod *Module) error {
// Check if the module was set up // Check if the module was set up
if _, ok := app.setupTracker[mod.name]; !ok { if _, ok := app.setupTracker[mod.name]; !ok {
return fmt.Errorf("module '%s' is not set up, cannot tear down", mod.name) return fmt.Errorf("module %s is not set up, cannot tear down", mod)
} }
// Run the teardown function for the module // Run the teardown function for the module
if mod.teardown != nil { if mod.teardown != nil {
if err := mod.teardown(mod); err != nil { if err := mod.teardown(mod); err != nil {
return fmt.Errorf("error tearing down '%s': %w", mod.name, err) return fmt.Errorf("error tearing down %s: %w", mod, err)
} }
} }

View File

@@ -3,6 +3,7 @@ package app
import ( import (
"errors" "errors"
"log/slog" "log/slog"
"os"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -11,20 +12,20 @@ import (
func TestNewLifecycle(t *testing.T) { func TestNewLifecycle(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
// Create a blank Lifecycle instance
lc := NewLifecycle() lc := NewLifecycle()
assert.NotNil(lc, "expected Lifecycle instance to be created") assert.NotNil(lc, "expected Lifecycle instance to be created")
assert.Equal(slog.Default(), lc.logger, "expected default logger to be set") assert.Equal(slog.Default(), lc.Logger(), "expected default logger to be available")
}
func TestNewLifecycleL(t *testing.T) {
assert := assert.New(t)
// Create a blank Lifecycle instance with a custom logger // Create a blank Lifecycle instance with a custom logger
logger := slog.Default() handler := slog.NewTextHandler(os.Stderr, nil)
lc := NewLifecycleL(logger) logger := slog.New(handler)
lc = NewLifecycle().WithOpts(LifecycleOpts{
Logger: logger,
})
assert.NotNil(lc) assert.NotNil(lc)
assert.Equal(logger, lc.logger, "expected logger to be set") assert.Equal(logger, lc.Logger(), "expected logger to match")
assert.Len(lc.modules, 0, "expected modules to be empty") assert.Len(lc.modules, 0, "expected modules to be empty")
assert.Len(lc.setupTracker, 0, "expected setup tracker to be empty") assert.Len(lc.setupTracker, 0, "expected setup tracker to be empty")
assert.Len(lc.teardownTracker, 0, "expected teardown tracker to be empty") assert.Len(lc.teardownTracker, 0, "expected teardown tracker to be empty")
@@ -33,15 +34,15 @@ func TestNewLifecycleL(t *testing.T) {
mod1 := NewModule("module1", ModuleOpts{}) mod1 := NewModule("module1", ModuleOpts{})
mod2 := NewModule("module2", ModuleOpts{}) mod2 := NewModule("module2", ModuleOpts{})
lcWithModules := NewLifecycleL(logger, mod1, mod2) lcWithModules := NewLifecycle(mod1, mod2)
assert.NotNil(lcWithModules, "expected Lifecycle with modules to be created") assert.NotNil(lcWithModules, "expected Lifecycle with modules to be created")
assert.Contains(lcWithModules.modules, mod1, "expected module1 to be included") assert.Contains(lcWithModules.modules, mod1, "expected module1 to be included")
assert.Contains(lcWithModules.modules, mod2, "expected module2 to be included") assert.Contains(lcWithModules.modules, mod2, "expected module2 to be included")
assert.Len(lcWithModules.modules, 2, "expected two modules to be present") assert.Len(lcWithModules.modules, 2, "expected two modules to be present")
// Test for duplicate module names // Test for duplicate module names
assert.PanicsWithValue("duplicate module name: module1", func() { assert.PanicsWithValue("duplicate module: [module1]", func() {
NewLifecycleL(logger, mod1, mod1) NewLifecycle(mod1, mod1)
}, "expected panic on duplicate module names") }, "expected panic on duplicate module names")
} }
@@ -57,17 +58,25 @@ func TestLifecycle_WithOpts(t *testing.T) {
assert.Equal(opts, lc.opts, "expected Lifecycle to have the provided options") assert.Equal(opts, lc.opts, "expected Lifecycle to have the provided options")
} }
func TestLifecycle_WithLogger(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle()
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
lc.WithLogger(logger)
assert.Equal(logger, lc.Logger(), "expected Lifecycle to have the provided logger")
}
func TestLifecycle_Logger(t *testing.T) { func TestLifecycle_Logger(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
lc := NewLifecycle() lc := NewLifecycle()
assert.Equal(slog.Default(), lc.Logger(), "expected Logger to return a logger") assert.Equal(slog.Default(), lc.Logger(), "expected Logger to return a logger")
// Test panic when logger is not initialized // Test that custom logger is used
lc.logger = nil logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
assert.PanicsWithValue("lifecycle logger is not initialized", func() { lc.opts.Logger = logger
lc.Logger() assert.Equal(logger, lc.Logger())
}, "expected Logger to panic when logger is not set")
} }
func TestLifecycle_Setup(t *testing.T) { func TestLifecycle_Setup(t *testing.T) {
@@ -190,6 +199,7 @@ func TestLifecycle_Teardown(t *testing.T) {
// Fake setup for all modules // Fake setup for all modules
for _, mod := range tc.modules { for _, mod := range tc.modules {
mod.loaded = true // Mark as loaded
lc.setupTracker[mod.name] = 0 // Mark as set up lc.setupTracker[mod.name] = 0 // Mark as set up
} }
@@ -200,7 +210,8 @@ func TestLifecycle_Teardown(t *testing.T) {
for _, mod := range tc.modules { for _, mod := range tc.modules {
if mod.teardown != nil { if mod.teardown != nil {
_, ok := lc.teardownTracker[mod.name] _, ok := lc.teardownTracker[mod.name]
assert.True(ok, "expected module to be marked as torn down") assert.Truef(ok, "expected module %s to be marked as torn down", mod)
assert.Falsef(mod.loaded, "expected module %s to be unloaded after teardown", mod)
} }
} }
} else { } else {
@@ -218,22 +229,33 @@ func TestLifecycle_Require(t *testing.T) {
mod := NewModule("module1", ModuleOpts{ mod := NewModule("module1", ModuleOpts{
Setup: func(m *Module) error { return nil }, Setup: func(m *Module) error { return nil },
}) })
lc.Require(mod) err := lc.Require(mod)
assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger") assert.NoError(t, err, "expected Require to succeed")
assert.Contains(t, lc.setupTracker, mod.name, "expected module to be marked as set up")
assert.Nil(t, mod.logger, "expected module logger to be nil")
assert.Equal(t, lc.Logger(), mod.Logger(), "expected module logger to match lifecycle logger")
} }
func TestLifecycle_RequireUnique(t *testing.T) { func TestLifecycle_RequireUnique(t *testing.T) {
assert := assert.New(t)
lc := NewLifecycle() lc := NewLifecycle()
mod := NewModule("module1", ModuleOpts{}) mod := NewModule("module1", ModuleOpts{})
err := lc.RequireUnique(mod) err := lc.RequireUnique(mod)
assert.NoError(t, err, "expected RequireL to succeed") assert.NoError(err, "expected RequireL to succeed")
assert.Equal(t, lc.logger, mod.logger, "expected module logger to match lifecycle logger") assert.Nil(mod.logger, "expected module logger to be nil")
// Requiring the same module again should fail
err = lc.RequireUnique(mod)
assert.Error(err, "expected RequireUnique to fail on duplicate module")
} }
func TestLifecycle_RequireL(t *testing.T) { func TestLifecycle_RequireL(t *testing.T) {
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
cases := []struct { cases := []struct {
name string name string
logger *slog.Logger
modules []*Module modules []*Module
existingModules []*Module existingModules []*Module
expectedErr string expectedErr string
@@ -245,7 +267,7 @@ func TestLifecycle_RequireL(t *testing.T) {
{ {
name: "nil module", name: "nil module",
modules: []*Module{nil}, modules: []*Module{nil},
expectedErr: "module is nil", expectedErr: "is nil",
}, },
{ {
name: "basic module", name: "basic module",
@@ -309,21 +331,16 @@ func TestLifecycle_RequireL(t *testing.T) {
lc := NewLifecycle(tc.existingModules...) lc := NewLifecycle(tc.existingModules...)
l := tc.logger err := lc.RequireL(logger, tc.modules...)
if l == nil {
l = lc.logger
}
err := lc.RequireL(tc.logger, tc.modules...)
if tc.expectedErr == "" { if tc.expectedErr == "" {
assert.NoError(err, "expected RequireL to succeed") assert.NoError(err, "expected RequireL to succeed")
for _, mod := range tc.modules { for i, mod := range tc.modules {
if mod != nil { if mod != nil {
_, ok := lc.setupTracker[mod.name] _, ok := lc.setupTracker[mod.name]
assert.True(ok, "expected module to be marked as set up") assert.Truef(ok, "expected module %d %s to be marked as set up", i, mod)
assert.True(mod.loaded, "expected module to be loaded") assert.Truef(mod.loaded, "expected module %d %s to be loaded", i, mod)
assert.Equal(l, mod.logger, "expected module logger to match") assert.Equalf(logger, mod.logger, "expected module %d %s logger to match", i, mod)
} }
} }
} else { } else {
@@ -335,6 +352,9 @@ func TestLifecycle_RequireL(t *testing.T) {
} }
func TestLifecycle_RequireUniqueL(t *testing.T) { func TestLifecycle_RequireUniqueL(t *testing.T) {
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
cases := []struct { cases := []struct {
name string name string
modules []*Module modules []*Module
@@ -354,12 +374,12 @@ func TestLifecycle_RequireUniqueL(t *testing.T) {
NewModule("module2", ModuleOpts{}), NewModule("module2", ModuleOpts{}),
}, },
existingModules: []string{"module1"}, existingModules: []string{"module1"},
expectedErr: "module 'module1' is already set up, cannot require it again", expectedErr: "module [module1] is already set up, cannot require it again",
}, },
{ {
name: "nil module", name: "nil module",
modules: []*Module{nil}, modules: []*Module{nil},
expectedErr: "module is nil", expectedErr: "is nil",
}, },
} }
@@ -373,7 +393,7 @@ func TestLifecycle_RequireUniqueL(t *testing.T) {
lc.setupTracker[modName] = 0 // Mark as set up lc.setupTracker[modName] = 0 // Mark as set up
} }
err := lc.RequireUniqueL(lc.logger, tc.modules...) err := lc.RequireUniqueL(logger, tc.modules...)
if tc.expectedErr == "" { if tc.expectedErr == "" {
assert.NoError(err, "expected RequireUniqueL to succeed") assert.NoError(err, "expected RequireUniqueL to succeed")
} else { } else {
@@ -385,6 +405,9 @@ func TestLifecycle_RequireUniqueL(t *testing.T) {
} }
func TestLifecycle_setupSingle(t *testing.T) { func TestLifecycle_setupSingle(t *testing.T) {
// Create a logger for the test cases
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
cases := []struct { cases := []struct {
name string name string
lc *Lifecycle lc *Lifecycle
@@ -447,7 +470,7 @@ func TestLifecycle_setupSingle(t *testing.T) {
Setup: func(m *Module) error { return errors.New("dependency setup failed") }, Setup: func(m *Module) error { return errors.New("dependency setup failed") },
}), }),
}, },
expectedErr: "error initializing 'errorModule'", expectedErr: "dependency setup failed",
}, },
{ {
name: "dependency without autoload", name: "dependency without autoload",
@@ -472,7 +495,7 @@ func TestLifecycle_setupSingle(t *testing.T) {
l := tc.logger l := tc.logger
if l == nil { if l == nil {
l = slog.Default() l = logger
} }
lc := tc.lc lc := tc.lc
@@ -480,20 +503,19 @@ func TestLifecycle_setupSingle(t *testing.T) {
lc = NewLifecycle(tc.modules...) lc = NewLifecycle(tc.modules...)
} }
err := lc.setupSingle(tc.targetModule, l) err := lc.setupSingle(l, tc.targetModule)
if tc.targetModule != nil {
assert.Equal(l, tc.targetModule.logger, "expected module logger to match")
}
if tc.expectedErr == "" { if tc.expectedErr == "" {
assert.NoError(err, "expected no error from setupSingle") assert.NoError(err, "expected no error from setupSingle")
} else { } else {
assert.Contains(err.Error(), tc.expectedErr, "expected error message to match") assert.Contains(err.Error(), tc.expectedErr, "expected error message to match")
} }
if tc.targetModule != nil {
assert.Equal(l, tc.targetModule.logger, "expected module logger to match")
}
}) })
} }
} }
func TestLifecycle_teardownSingle(t *testing.T) { func TestLifecycle_teardownSingle(t *testing.T) {

View File

@@ -13,9 +13,17 @@ var ErrModuleNotFound = errors.New("module not found")
// moduleFn is a function type for module setup and teardown functions. // moduleFn is a function type for module setup and teardown functions.
type moduleFn func(*Module) error type moduleFn func(*Module) error
// GenericModule is an interface that allows modules to be extended with custom
// functionality, as the module can return a pointer to the underlying Module.
type GenericModule interface {
// Module returns the underlying Module instance.
Module() *Module
}
// Module represents a sub-system of the application, with its setup and // Module represents a sub-system of the application, with its setup and
// teardown functions and dependencies. // teardown functions and dependencies.
type Module struct { type Module struct {
lifecycle *Lifecycle // lifecycle is the parent lifecycle this module belongs to
logger *slog.Logger logger *slog.Logger
name string name string
setup moduleFn setup moduleFn
@@ -45,19 +53,25 @@ func NewModule(name string, opts ModuleOpts) *Module {
} }
} }
// Logger returns the logger for the module. // Logger returns the logger for the module. Uses the lifecycle's logger unless
// a specific logger has been set during module load.
func (s *Module) Logger() *slog.Logger { func (s *Module) Logger() *slog.Logger {
if s.logger == nil { if s.logger != nil {
panic(fmt.Sprintf("module %s used before logger was initialized", s.name))
}
return s.logger return s.logger
} }
return s.lifecycle.Logger()
}
// Name returns the name of the module. // Name returns the name of the module.
func (s *Module) Name() string { func (s *Module) Name() string {
return s.name return s.name
} }
// String returns the name of the module in square brackets, e.g. "[module-name]".
func (s *Module) String() string {
return fmt.Sprintf("[%s]", s.name)
}
// Loaded returns whether the module has been set up. // Loaded returns whether the module has been set up.
func (s *Module) Loaded() bool { func (s *Module) Loaded() bool {
return s.loaded return s.loaded