diff --git a/lifecycle.go b/lifecycle.go index 5d571f6..e2cf10f 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -5,10 +5,10 @@ import ( "log/slog" ) -// setupFn is a function that runs setup logic for a sub-system. +// setupFn is a function that runs setup logic for a module. type setupFn func() error -// teardownFn is a function that runs teardown logic for a sub-system. +// teardownFn is a function that runs teardown logic for a module. type teardownFn func() // LifecycleOpts contains user-exposed options when defining a lifecycle. @@ -18,38 +18,44 @@ type LifecycleOpts struct { } // Lifecycle represents the core application structure. Lifecycle manages -// resources providing an orchestrator for setup and teardown of sub-systems. +// resources providing an orchestrator for setup and teardown of modules. 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 - subsystems []*Subsystem + modules []*Module setupTracker map[string]bool teardownTracker map[string]bool opts LifecycleOpts } // NewLifecycle creates a new Lifecycle instance with a default logger and the -// given subsystems. It panics if any subsystem has a duplicate name. -func NewLifecycle(defaultLogger *slog.Logger, subsystems ...*Subsystem) *Lifecycle { +// given modules. It panics if any module has a duplicate name. +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 subsystems are unique + // Ensure modules are unique unique := make(map[string]bool) - for _, sub := range subsystems { + for _, sub := range modules { if _, exists := unique[sub.name]; exists { - panic(fmt.Sprintf("duplicate subsystem name: %s", sub.name)) + panic(fmt.Sprintf("duplicate module name: %s", sub.name)) } unique[sub.name] = true } return &Lifecycle{ logger: defaultLogger, - subsystems: subsystems, + modules: modules, setupTracker: make(map[string]bool), teardownTracker: make(map[string]bool), } @@ -61,13 +67,21 @@ func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle { return app } -// Setup initializes all sub-systems in the order they were defined and checks -// for dependencies. It returns an error if any sub-system fails to initialize or +// Logger returns the logger for the lifecycle. +func (app *Lifecycle) Logger() *slog.Logger { + if app.logger == nil { + panic("lifecycle logger is not initialized") + } + return app.logger +} + +// Setup initializes all modules in the order they were defined and checks +// for dependencies. It returns an error if any module fails to initialize or // if dependencies are not satisfied. Lifecycle.Teardown should always be run at the end // of the application lifecycle to ensure all resources are cleaned up properly. func (app *Lifecycle) Setup() error { setupCount := 0 - for _, sub := range app.subsystems { + for _, sub := range app.modules { if ok, err := app.setupSingle(sub, app.logger); err != nil { return err } else if ok { @@ -75,76 +89,76 @@ func (app *Lifecycle) Setup() error { } } - app.logger.Info("Lifecycle sub-systems initialized, backend setup complete", - "count", setupCount, "subsystems", mapToString(app.setupTracker)) + app.logger.Info("Lifecycle modules initialized, backend setup complete", + "count", setupCount, "modules", mapToString(app.setupTracker)) return nil } -// Teardown runs all sub-system teardown functions in reverse order of setup. +// Teardown runs all module teardown functions in reverse order of setup. // Teardown should always be run at the end of the application lifecycle // to ensure all resources are cleaned up properly. func (app *Lifecycle) Teardown() error { teardownCount := 0 - for i := len(app.subsystems) - 1; i >= 0; i-- { - if ok, err := app.singleTeardown(app.subsystems[i]); err != nil { + for i := len(app.modules) - 1; i >= 0; i-- { + if ok, err := app.singleTeardown(app.modules[i]); err != nil { return err } else if ok { teardownCount++ } } - app.logger.Info("All sub-systems torn down, backend teardown complete", - "count", teardownCount, "subsystems", mapToString(app.teardownTracker)) + app.logger.Info("All modules torn down, backend teardown complete", + "count", teardownCount, "modules", mapToString(app.teardownTracker)) return nil } -// Require adds sub-system(s) to the lifecycle and immediately runs any setup -// functions. Relevant when a sub-system is not part of the main application -// but may still be conditionally necessary. Any sub-systems that are already +// Require adds module(s) to the lifecycle and immediately runs any setup +// functions. Relevant when a module is not part of the main application +// but may still be conditionally necessary. Any modules that are already // set up are ignored. -func (app *Lifecycle) Require(subsystems ...*Subsystem) error { - return app.RequireL(app.logger, subsystems...) +func (app *Lifecycle) Require(modules ...*Module) error { + return app.RequireL(app.logger, modules...) } -// RequireL adds sub-system(s) to the lifecycle with a specific logger and +// 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 sub-systems with a non- +// This variation is useful when you need to set up modules with a non- // default logger. -func (app *Lifecycle) RequireL(logger *slog.Logger, subsystems ...*Subsystem) error { - for _, sub := range subsystems { +func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { + for _, sub := range modules { if sub == nil { - return fmt.Errorf("sub-system is nil") + return fmt.Errorf("module is nil") } - // Check if the sub-system is already set up + // Check if the module is already set up if _, ok := app.setupTracker[sub.name]; ok { - app.logger.Warn("sub-system already set up, ignoring", "subsystem", sub.name) + app.logger.Warn("module already set up, ignoring", "module", sub.name) continue } - // Add the sub-system to the lifecycle - app.subsystems = append(app.subsystems, sub) + // Add the module to the lifecycle + app.modules = append(app.modules, sub) - // Run the setup function for the sub-system + // Run the setup function for the module if _, err := app.setupSingle(sub, logger); err != nil { - return fmt.Errorf("error setting up required subsystem '%s': %w", sub.name, err) + return fmt.Errorf("error setting up required module '%s': %w", sub.name, err) } } - app.logger.Info("New sub-systems initialized", "all", mapToString(app.setupTracker)) + app.logger.Info("New modules initialized", "all", mapToString(app.setupTracker)) return nil } -// setupSingle is a helper function to set up a single sub-system. -func (app *Lifecycle) setupSingle(sub *Subsystem, logger *slog.Logger) (bool, error) { +// setupSingle is a helper function to set up a single module. +func (app *Lifecycle) setupSingle(sub *Module, logger *slog.Logger) (bool, error) { if sub == nil { - return false, fmt.Errorf("sub-system is nil") + return false, fmt.Errorf("module is nil") } - // Set the logger for the sub-system + // Set the logger for the module sub.logger = logger // Check if all dependencies are satisfied @@ -163,12 +177,12 @@ func (app *Lifecycle) setupSingle(sub *Subsystem, logger *slog.Logger) (bool, er } if sub.setup != nil { - // Run the setup function for the sub-system + // Run the setup function for the module if err := sub.setup(); err != nil { return false, fmt.Errorf("error initializing '%s': %w", sub.name, err) } - // Mark this subsystem as setup + // Mark this module as setup app.setupTracker[sub.name] = true return true, nil } @@ -176,22 +190,22 @@ func (app *Lifecycle) setupSingle(sub *Subsystem, logger *slog.Logger) (bool, er return false, nil } -// singleTeardown is a helper function to tear down a single sub-system. -func (app *Lifecycle) singleTeardown(sub *Subsystem) (bool, error) { +// singleTeardown is a helper function to tear down a single module. +func (app *Lifecycle) singleTeardown(sub *Module) (bool, error) { if sub == nil { - return false, fmt.Errorf("sub-system is nil") + return false, fmt.Errorf("module is nil") } - // Check if the sub-system was set up + // Check if the module was set up if _, ok := app.setupTracker[sub.name]; !ok { return false, nil } - // Run the teardown function for the sub-system + // Run the teardown function for the module if sub.teardown != nil { sub.teardown() - // Mark this subsystem as torn down + // Mark this module as torn down app.teardownTracker[sub.name] = true return true, nil } diff --git a/module.go b/module.go new file mode 100644 index 0000000..d30c946 --- /dev/null +++ b/module.go @@ -0,0 +1,49 @@ +package app + +import ( + "fmt" + "log/slog" +) + +// Module represents a sub-system of the application, with its setup and +// teardown functions and dependencies. +type Module struct { + logger *slog.Logger + name string + setup setupFn + teardown teardownFn + depends []*Module +} + +// ModuleOpts contains user-exposed options when defining a module. +type ModuleOpts struct { + // Setup is the setup function for the module. + Setup setupFn + // Teardown is the teardown function for the module. + Teardown teardownFn + // Depends is a list of modules that this module depends on. + Depends []*Module +} + +// NewModule creates a new Module instance with the given name and options. +func NewModule(name string, opts ModuleOpts) *Module { + return &Module{ + name: name, + setup: opts.Setup, + teardown: opts.Teardown, + depends: opts.Depends, + } +} + +// Logger returns the logger for the module. +func (s *Module) Logger() *slog.Logger { + if s.logger == nil { + panic(fmt.Sprintf("subsytem %s used before logger was initialized", s.name)) + } + return s.logger +} + +// Name returns the name of the module. +func (s *Module) Name() string { + return s.name +} diff --git a/subsystem.go b/subsystem.go deleted file mode 100644 index 7da7e01..0000000 --- a/subsystem.go +++ /dev/null @@ -1,44 +0,0 @@ -package app - -import ( - "fmt" - "log/slog" -) - -// Subsystem represents a sub-system of the application, with its setup and -// teardown functions and dependencies. -type Subsystem struct { - logger *slog.Logger - name string - setup setupFn - teardown teardownFn - depends []*Subsystem -} - -// SubsystemOpts contains user-exposed options when defining a subsystem. -type SubsystemOpts struct { - // Setup is the setup function for the subsystem. - Setup setupFn - // Teardown is the teardown function for the subsystem. - Teardown teardownFn - // Depends is a list of subsystems that this subsystem depends on. - Depends []*Subsystem -} - -// NewSubsystem creates a new Subsystem instance with the given name and options. -func NewSubsystem(name string, opts SubsystemOpts) *Subsystem { - return &Subsystem{ - name: name, - setup: opts.Setup, - teardown: opts.Teardown, - depends: opts.Depends, - } -} - -// Logger returns the logger for the subsystem. -func (s *Subsystem) Logger() *slog.Logger { - if s.logger == nil { - panic(fmt.Sprintf("subsytem %s used before logger was initialized", s.name)) - } - return s.logger -}