diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfa8af2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Test coverage +coverage.out +coverage.html + +# Go build artifacts +*.test diff --git a/README.md b/README.md index 7fcad49..4ff9805 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # app -app is a simple and lightweight app lifecycle management library. +app orchestrates modular application setup and teardown for Go services. -## Example +## Install -Keep your `main.go` as simple as possible, all it's responsible for is creating the lifecycle, defining a default logger, and orchestrating setup and teardown. +```bash +go get gitea.auvem.com/go-toolkit/app +``` -**main.go** +## Quick start ```go package main @@ -16,75 +18,74 @@ import ( "fmt" "log/slog" + "gitea.auvem.com/go-toolkit/applog" "gitea.auvem.com/go-toolkit/app" ) func main() { - // Create a new lifecycle, defaulting to no printed logs - lifecycle := app.NewLifecycle().WithLogger(slog.New(slog.DiscardHandler)) + lifecycle := app.NewLifecycle( + applog.AppLogOpts{ConsoleOutput: os.Stderr}.Module(), + ) defer func() { if err := lifecycle.Teardown(); err != nil { - fmt.Println("Error during shutdown", err) + fmt.Println("shutdown error:", err) } }() - // Encodes the lifecycle into a context to be used downstream - ctx := lifecycle.Context(context.Background()) - - // Off to you, call your entrypoint here. - Hello(ctx) -} -``` - -Now, let's define a basic logging module and handle loading that module. - -**logger.go** - -```go -package main - -import ( - "log/slog" - - "gitea.auvem.com/go-toolkit/app" -) - -func ModuleLog(setDefault bool) *app.Module { - return app.NewModule("logger", app.ModuleOpts{ - Setup: func(m *app.Module) error { - handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}) - if setDefault { - slog.SetDefault(handler) - } - - m.Lifecycle().WithLogger(handler) - } - }) -} -``` - -Logging can, however, be somewhat of a complex operation in itself, so I'd highly recommend checking out my [applog](https://gitea.auvem.com/go-toolkit/applog) project, which features tight integration with this app. - -Finally, lets put our new lifecycle (and logger) to work! - -**hello.go** - -```go -package main - -import ( - "context" - - "gitea.auvem.com/go-toolkit/app" -) - -func Hello(ctx context.Context) { - app := app.LifecycleFromContext(ctx) - if app == nil { - panic("hello must run within an app lifecycle") + if err := lifecycle.Setup(); err != nil { + panic(err) } - lifecycle.Require(ModuleLog(true)) - app.Logger().Info("Hello world!", "foo", "bar") + ctx := lifecycle.Context(context.Background()) + _ = ctx + lifecycle.Logger().Info("ready") } ``` + +## Concepts + +- **Module** — a named subsystem with optional `Setup`, `Teardown`, and `Depends` +- **Lifecycle** — holds modules and runs setup/teardown in dependency order +- **Setup** — initializes all modules registered via `NewLifecycle` +- **Require** — adds and initializes modules on demand (typical for CLI subcommands via [appcli](https://gitea.auvem.com/go-toolkit/appcli)) +- **Autoload** — when enabled (default), `Depends` names are set up automatically before the dependent module + +## Dependencies + +List dependency module **names** in `ModuleOpts.Depends`. Dependent modules must be registered in the same lifecycle (via `NewLifecycle` or an earlier `Require`). Circular dependencies return an error. + +## Teardown + +Always defer `lifecycle.Teardown()`. Teardown runs in **reverse setup order** so dependents shut down before their dependencies. + +## Context + +Use `lifecycle.Context(ctx)` so downstream code and `appcli` commands can call `app.LifecycleFromContext(ctx)`. + +## Companion packages + +| Package | Role | +|---------|------| +| [applog](https://gitea.auvem.com/go-toolkit/applog) | slog + tint logging module | +| [appcli](https://gitea.auvem.com/go-toolkit/appcli) | urfave/cli v3 integration | +| [dbx](https://gitea.auvem.com/go-toolkit/dbx) | database module | +| [migrate](https://gitea.auvem.com/go-toolkit/migrate) | goose migrations module | + +## API reference + +| Symbol | Description | +|--------|-------------| +| `NewLifecycle(modules...)` | Create lifecycle with optional initial modules | +| `Lifecycle.Setup()` | Set up all registered modules | +| `Lifecycle.Teardown()` | Tear down in reverse setup order | +| `Lifecycle.Require(modules...)` | Set up modules on demand | +| `Lifecycle.RequireWithOpts(opts, modules...)` | Require with custom logger or unique-name enforcement | +| `Lifecycle.GetModule(name)` | Look up a registered module | +| `NewModule(name, opts)` | Define a module | +| `LifecycleFromContext(ctx)` | Retrieve lifecycle from context | + +## Error handling + +- Partial setup failure rolls back already-initialized modules +- `Teardown` joins errors from all modules but attempts every teardown +- A second `Setup` or `Teardown` on the same lifecycle returns an error diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..60977ac --- /dev/null +++ b/doc.go @@ -0,0 +1,26 @@ +// Package app orchestrates modular application setup and teardown. +// +// Register subsystems as [Module] values, attach them to a [Lifecycle], and call +// [Lifecycle.Setup] or [Lifecycle.Require] to initialize resources in dependency +// order. Always call [Lifecycle.Teardown] (typically via defer) to release +// resources in reverse setup order. +// +// Module authors (dbx, applog, migrate, etc.) and main/cmd packages both import +// app. Use [Lifecycle.Context] to propagate the lifecycle through context.Context +// for CLI commands via [gitea.auvem.com/go-toolkit/appcli]. +// +// # Concepts +// +// - [Module] — named unit with optional Setup, Teardown, and Depends +// - [Lifecycle] — registry and orchestrator for modules +// - Setup order — dependencies first; autoload resolves Depends when enabled +// - Require — lazy/conditional setup (common in CLI subcommands) +// +// # API overview +// +// Lifecycle — [NewLifecycle], [Lifecycle.Setup], [Lifecycle.Teardown], +// [Lifecycle.Require], [Lifecycle.RequireWithOpts], [Lifecycle.GetModule], +// [Lifecycle.Context], [LifecycleFromContext] +// +// Module — [NewModule], [Module.Lifecycle], [Module.Logger], [Module.Loaded] +package app