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:
8
go.mod
8
go.mod
@@ -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
10
go.sum
Normal 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=
|
||||||
141
lifecycle.go
141
lifecycle.go
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
22
module.go
22
module.go
@@ -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,12 +53,13 @@ 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.
|
||||||
@@ -58,6 +67,11 @@ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user