diff --git a/README.md b/README.md index 8d4db2e..839c98e 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ See [Architecture Guide](docs/architecture.md) for details. - [Authentication](docs/guides/authentication.md) — Auth middleware and guards - [Context Helpers](docs/guides/context-helpers.md) — Typed context keys and helper functions - [Testing](docs/guides/testing.md) — Testing patterns +- [NestJS Compatibility](docs/guides/nestjs-compatibility.md) — Feature parity and Go-idiomatic differences - [Comparison](docs/guides/comparison.md) — vs Wire, Fx, and others **Reference:** diff --git a/docs/guides/lifecycle.md b/docs/guides/lifecycle.md index db2c907..2ec966c 100644 --- a/docs/guides/lifecycle.md +++ b/docs/guides/lifecycle.md @@ -33,8 +33,8 @@ stateDiagram-v2 end note note right of Cleanup - Manual cleanup required - (no automatic hooks) + Call App.Close/CloseContext + (closes io.Closer providers) end note ``` @@ -133,40 +133,34 @@ fmt.Println(svc1 == svc2) // true ## Cleanup and Shutdown -modkit does not provide automatic cleanup hooks. You must manage cleanup manually: +modkit provides explicit shutdown via `App.Close()` / `App.CloseContext(ctx)`. Providers that implement +`io.Closer` are closed in reverse build order when you call these methods. -### App.Close and CloseContext - -If providers implement `io.Closer`, you can shut down the app explicitly: - -```go -// Close all io.Closer providers in reverse build order. -if err := app.Close(); err != nil { - log.Printf("shutdown error: %v", err) -} -``` - -`Close()` is idempotent: once it completes successfully (even with aggregated errors), -subsequent calls return `nil` and do not re-close providers. - -For context-aware shutdown, use `CloseContext(ctx)`: +### Primary Pattern: App.Close/CloseContext ```go -ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) -defer cancel() +func main() { + app, err := kernel.Bootstrap(&AppModule{}) + if err != nil { + log.Fatal(err) + } -if err := app.CloseContext(ctx); err != nil { - // Returns ctx.Err() if canceled or timed out - log.Printf("shutdown error: %v", err) + router := mkhttp.NewRouter() + mkhttp.RegisterRoutes(mkhttp.AsRouter(router), app.Controllers) + if err := mkhttp.Serve(":8080", router); err != nil { + log.Printf("server error: %v", err) + } + if err := app.CloseContext(context.Background()); err != nil { + log.Printf("shutdown error: %v", err) + } } ``` -`CloseContext` checks `ctx.Err()` before closing and before each closer. If the context -is canceled mid-close, it returns the context error and leaves the app eligible for -a later `Close()` retry. While a close is in progress, concurrent close calls are -no-ops. +`CloseContext` checks `ctx.Err()` before starting and before each provider close. If the context +is canceled, it returns `ctx.Err()` and leaves the app eligible for a later `Close()` retry. +Use `App.Close()` if you don't need context-aware cancellation behavior. -### Pattern 1: Cleanup in main() +### Alternative 1: Cleanup in main() ```go func main() { @@ -191,7 +185,7 @@ func main() { } ``` -### Pattern 2: Cleanup Provider +### Alternative 2: Cleanup Provider Create a provider that tracks resources needing cleanup: @@ -242,7 +236,7 @@ func main() { } ``` -### Pattern 3: Context-Based Shutdown +### Alternative 3: Context-Based Shutdown Use context cancellation for coordinated shutdown: @@ -257,10 +251,15 @@ func main() { // Set up signal handling sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) - + go func() { <-sigCh - cancel() // Signal shutdown + cancel() // Signal shutdown to providers using ctx + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + if err := app.CloseContext(shutdownCtx); err != nil { + log.Printf("shutdown error: %v", err) + } }() // Start server @@ -304,9 +303,9 @@ See [Context Helpers Guide](context-helpers.md) for typed context patterns. | Aspect | Fx | modkit | |--------|-----|--------| -| Lifecycle hooks | `OnStart`/`OnStop` | Manual cleanup | +| Lifecycle hooks | `OnStart`/`OnStop` | `App.Close()` / `CloseContext` | | Scopes | Singleton, request, custom | Singleton only | -| Automatic cleanup | Yes | No | +| Automatic cleanup | Yes | Explicit close | ### vs NestJS @@ -314,7 +313,7 @@ See [Context Helpers Guide](context-helpers.md) for typed context patterns. |--------|--------|--------| | Scopes | Singleton, Request, Transient | Singleton only | | `onModuleInit` | Provider hook | Not supported | -| `onModuleDestroy` | Provider hook | Manual cleanup | +| `onModuleDestroy` | Provider hook | `App.Close()` / `CloseContext` | | Request-scoped | Framework-managed | Use `context.Context` | ## Best Practices @@ -328,8 +327,8 @@ See [Context Helpers Guide](context-helpers.md) for typed context patterns. - Let providers build when first needed 3. **Track resources that need cleanup** - - Use defer in `main()` - - Or create a cleanup provider pattern + - Implement `io.Closer` and call `app.Close()` / `CloseContext` + - Or use manual patterns for custom cleanup 4. **Use context for request-scoped data** - Don't try to make request-scoped providers diff --git a/docs/guides/modules.md b/docs/guides/modules.md index 7daea75..26c3865 100644 --- a/docs/guides/modules.md +++ b/docs/guides/modules.md @@ -174,6 +174,30 @@ Rules: - A module can access tokens exported by modules it imports - Accessing non-visible tokens returns `TokenNotVisibleError` +## Re-exporting Tokens + +You can re-export tokens from imported modules to create a public facade: + +```go +const TokenDB module.Token = "database.connection" +const TokenUsersService module.Token = "users.service" + +type AppModule struct { + db *DatabaseModule + users *UsersModule +} + +func (m *AppModule) Definition() module.ModuleDef { + return module.ModuleDef{ + Name: "app", + Imports: []module.Module{m.db, m.users}, + Exports: []module.Token{TokenDB, TokenUsersService}, + } +} +``` + +Visibility still applies: a module can only re-export tokens that are exported by its imports (or its own providers). Re-exporting does not bypass visibility rules. + ## Common Patterns ### Shared Database Module diff --git a/docs/guides/nestjs-compatibility.md b/docs/guides/nestjs-compatibility.md new file mode 100644 index 0000000..f56c2f3 --- /dev/null +++ b/docs/guides/nestjs-compatibility.md @@ -0,0 +1,244 @@ +# NestJS Compatibility Guide + +This guide maps NestJS concepts to modkit. It highlights what is implemented, what is intentionally skipped, and the Go-idiomatic alternatives. + +## Feature Matrix + +| Category | NestJS Feature | modkit Status | Notes | +|----------|----------------|---------------|-------| +| Modules | Module definition | ✅ Implemented | `ModuleDef` struct vs `@Module()` decorator | +| Modules | Imports | ✅ Implemented | Same concept | +| Modules | Exports | ✅ Implemented | Same concept | +| Modules | Providers | ✅ Implemented | Same concept | +| Modules | Controllers | ✅ Implemented | Same concept | +| Modules | Global modules | ⏭️ Skipped | Prefer explicit imports | +| Modules | Dynamic modules | ⏭️ Different | Use constructor functions with options | +| Modules | Module re-exporting | 🔄 Epic 02 | Export tokens from imported modules | +| Providers | Singleton scope | ✅ Implemented | Default and only scope | +| Providers | Request scope | ⏭️ Skipped | Use `context.Context` instead | +| Providers | Transient scope | ⏭️ Skipped | Use factory functions if needed | +| Providers | useClass | ✅ Implemented | `Build` function returning a concrete type | +| Providers | useValue | ✅ Implemented | `Build` returns a static value | +| Providers | useFactory | ✅ Implemented | `Build` is the factory | +| Providers | useExisting | ⏭️ Skipped | Use token aliases in `Build` | +| Providers | Async providers | ⏭️ Different | Go is sync; use goroutines if needed | +| Lifecycle | onModuleInit | ⏭️ Skipped | Put init logic in `Build()` | +| Lifecycle | onApplicationBootstrap | ⏭️ Skipped | Controllers built = app bootstrapped | +| Lifecycle | onModuleDestroy | 🔄 Epic 02 | Via `io.Closer` interface | +| Lifecycle | beforeApplicationShutdown | ⏭️ Skipped | Covered by `io.Closer` | +| Lifecycle | onApplicationShutdown | 🔄 Epic 02 | `App.Close()` method | +| Lifecycle | enableShutdownHooks | ⏭️ Different | Use `signal.NotifyContext` | +| HTTP | Controllers | ✅ Implemented | `RouteRegistrar` interface | +| HTTP | Route decorators | ⏭️ Different | Explicit `RegisterRoutes()` method | +| HTTP | Middleware | ✅ Implemented | Standard `func(http.Handler) http.Handler` | +| HTTP | Guards | ⏭️ Different | Implement as middleware | +| HTTP | Interceptors | ⏭️ Different | Implement as middleware | +| HTTP | Pipes | ⏭️ Different | Validate in handler or middleware | +| HTTP | Exception filters | ⏭️ Different | Error handling middleware | +| Other | CLI scaffolding | ❌ Not planned | Go boilerplate is minimal | +| Other | Devtools | ❌ Not planned | Use standard Go tooling | +| Other | Microservices | ❌ Not planned | Out of scope | +| Other | WebSockets | ❌ Not planned | Use `gorilla/websocket` directly | +| Other | GraphQL | ❌ Not planned | Use `gqlgen` directly | + +## Justifications + +### Global Modules + +**NestJS:** `@Global()` makes a module’s exports available everywhere without explicit imports. + +**modkit:** Skipped. + +**Why:** Global modules hide dependencies, which conflicts with Go’s explicit import and visibility conventions. + +**Alternative:** Construct the module once and import it explicitly wherever needed. + +### Dynamic Modules + +**NestJS:** `forRoot()`/`forRootAsync()` return module definitions at runtime. + +**modkit:** Different. + +**Why:** Go favors explicit constructors over dynamic metadata. + +**Alternative:** Use module constructor functions that accept options and return a configured module instance. + +### Request Scope + +**NestJS:** Providers can be scoped per request. + +**modkit:** Skipped. + +**Why:** Per-request DI adds hidden lifecycle complexity in Go. + +**Alternative:** Pass `context.Context` explicitly and construct request-specific values in handlers or middleware. + +### Transient Scope + +**NestJS:** Providers can be created on every injection. + +**modkit:** Skipped. + +**Why:** It encourages implicit, hidden object graphs. + +**Alternative:** Use factory functions in `Build` or plain constructors where you need new instances. + +### useExisting + +**NestJS:** Alias one provider token to another with `useExisting`. + +**modkit:** Skipped. + +**Why:** Aliasing is simple and explicit in Go. + +**Alternative:** Resolve the original token in `Build` and return it under the new token. + +### Async Providers + +**NestJS:** Providers can be async and awaited. + +**modkit:** Different. + +**Why:** Go initialization is synchronous; async is explicit and opt-in. + +**Alternative:** Start goroutines in `Build` or expose `Start()` methods explicitly. + +### onModuleInit + +**NestJS:** Lifecycle hook invoked after a module is initialized. + +**modkit:** Skipped. + +**Why:** Initialization belongs in the constructor/build path in Go. + +**Alternative:** Put setup logic in `Build()` or explicit `Start()` methods. + +### onApplicationBootstrap + +**NestJS:** Hook after the app finishes bootstrapping. + +**modkit:** Skipped. + +**Why:** modkit bootstraps deterministically when controllers are built. + +**Alternative:** Use explicit post-bootstrap calls in `main`. + +### beforeApplicationShutdown + +**NestJS:** Hook before shutdown. + +**modkit:** Skipped. + +**Why:** Cleanup is modeled via `io.Closer` in Go. + +**Alternative:** Implement `Close()` on providers and call `App.Close()`. + +### enableShutdownHooks + +**NestJS:** Enables signal handling for graceful shutdown. + +**modkit:** Different. + +**Why:** Go already provides standard signal handling primitives. + +**Alternative:** Use `signal.NotifyContext` and call `App.Close()` and `http.Server.Shutdown()` explicitly. + +### Route Decorators + +**NestJS:** Decorators define routes on methods. + +**modkit:** Different. + +**Why:** Go does not use decorators or reflection for routing. + +**Alternative:** Implement `RegisterRoutes(router)` and bind handlers explicitly. + +### Guards + +**NestJS:** Guard hooks control route access. + +**modkit:** Different. + +**Why:** Go middleware is the standard control point. + +**Alternative:** Implement guards as `func(http.Handler) http.Handler` middleware. + +### Interceptors + +**NestJS:** Wrap request/response with cross-cutting logic. + +**modkit:** Different. + +**Why:** Go uses middleware for cross-cutting concerns. + +**Alternative:** Implement as middleware or handler wrappers. + +### Pipes + +**NestJS:** Transform/validate input via pipes. + +**modkit:** Different. + +**Why:** Go favors explicit validation near the handler. + +**Alternative:** Validate in handlers or middleware using standard libraries. + +### Exception Filters + +**NestJS:** Centralized exception handling layer. + +**modkit:** Different. + +**Why:** Errors are values in Go; handling is explicit. + +**Alternative:** Use error-handling middleware or helpers that return `Problem Details` responses. + +### CLI Scaffolding + +**NestJS:** CLI generates boilerplate. + +**modkit:** Not planned. + +**Why:** Go projects are minimal and tooling is already strong. + +**Alternative:** Use `go generate` or project templates if you want scaffolding. + +### Devtools + +**NestJS:** Devtools for inspection and debugging. + +**modkit:** Not planned. + +**Why:** Go relies on standard tooling and observability. + +**Alternative:** Use `pprof`, logging, and standard debug tools. + +### Microservices + +**NestJS:** Built-in microservices framework. + +**modkit:** Not planned. + +**Why:** Out of modkit’s scope as a minimal backend framework. + +**Alternative:** Use dedicated Go libraries for gRPC, NATS, or Kafka. + +### WebSockets + +**NestJS:** WebSocket gateway abstractions. + +**modkit:** Not planned. + +**Why:** Go already has solid standalone libraries. + +**Alternative:** Use `gorilla/websocket` directly. + +### GraphQL + +**NestJS:** GraphQL module and decorators. + +**modkit:** Not planned. + +**Why:** Go has a strong, explicit GraphQL ecosystem. + +**Alternative:** Use `gqlgen` directly. diff --git a/examples/hello-mysql/cmd/api/main.go b/examples/hello-mysql/cmd/api/main.go index 727b12f..e66e4eb 100644 --- a/examples/hello-mysql/cmd/api/main.go +++ b/examples/hello-mysql/cmd/api/main.go @@ -54,7 +54,7 @@ func main() { errCh <- server.ListenAndServe() }() - hooks := lifecycle.FromFuncs(boot.CleanupHooks()) + hooks := buildShutdownHooks(boot) if err := runServer(modkithttp.ShutdownTimeout, server, sigCh, errCh, hooks); err != nil { log.Fatalf("server failed: %v", err) } @@ -101,6 +101,16 @@ type shutdownServer interface { Shutdown(context.Context) error } +type appLifecycle interface { + CleanupHooks() []func(context.Context) error + CloseContext(context.Context) error +} + +func buildShutdownHooks(app appLifecycle) []lifecycle.CleanupHook { + hooks := lifecycle.FromFuncs(app.CleanupHooks()) + return append([]lifecycle.CleanupHook{app.CloseContext}, hooks...) +} + func runServer(shutdownTimeout time.Duration, server shutdownServer, sigCh <-chan os.Signal, errCh <-chan error, hooks []lifecycle.CleanupHook) error { select { case err := <-errCh: diff --git a/examples/hello-mysql/cmd/api/main_test.go b/examples/hello-mysql/cmd/api/main_test.go index dcce464..cbd17d4 100644 --- a/examples/hello-mysql/cmd/api/main_test.go +++ b/examples/hello-mysql/cmd/api/main_test.go @@ -19,6 +19,28 @@ type stubServer struct { shutdownCh chan struct{} } +type stubApp struct { + closeCalled bool + cleanupCalled bool + order []string +} + +func (s *stubApp) CleanupHooks() []func(context.Context) error { + return []func(context.Context) error{ + func(ctx context.Context) error { + s.cleanupCalled = true + s.order = append(s.order, "cleanup") + return nil + }, + } +} + +func (s *stubApp) CloseContext(ctx context.Context) error { + s.closeCalled = true + s.order = append(s.order, "close") + return nil +} + func (s *stubServer) ListenAndServe() error { return nil } @@ -62,6 +84,29 @@ func TestRunServer_ShutdownPath(t *testing.T) { } } +func TestBuildShutdownHooks_AppCloseRunsLast(t *testing.T) { + app := &stubApp{} + + hooks := buildShutdownHooks(app) + if len(hooks) != 2 { + t.Fatalf("expected 2 hooks, got %d", len(hooks)) + } + + if err := lifecycle.RunCleanup(context.Background(), hooks); err != nil { + t.Fatalf("unexpected cleanup error: %v", err) + } + + if !app.cleanupCalled { + t.Fatal("expected cleanup hook to run") + } + if !app.closeCalled { + t.Fatal("expected CloseContext to run") + } + if len(app.order) != 2 || app.order[0] != "cleanup" || app.order[1] != "close" { + t.Fatalf("expected cleanup then close, got %v", app.order) + } +} + func TestRunServer_ReturnsListenError(t *testing.T) { server := &stubServer{} sigCh := make(chan os.Signal, 1) diff --git a/go.work.sum b/go.work.sum index 6c25120..4b643e5 100644 --- a/go.work.sum +++ b/go.work.sum @@ -22,7 +22,10 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUu github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38= github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -35,10 +38,12 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -166,6 +171,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92Bcuy github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -174,6 +180,7 @@ github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXc github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= @@ -182,7 +189,9 @@ github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+l github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/intel/goresctrl v0.3.0 h1:K2D3GOzihV7xSBedGxONSlaw/un1LZgWsc9IfqipN4c= github.com/intel/goresctrl v0.3.0/go.mod h1:fdz3mD85cmP9sHD8JUlrNWAxvwM86CrbmVXltEKd7zk= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= @@ -226,6 +235,7 @@ github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= @@ -258,6 +268,7 @@ github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d h1:CdDQnGF8Nq9ocOS/xlSptM1N3BbrA6/kmaep5ggwaIA= github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d/go.mod h1:3OzsM7FXDQlpCiw2j81fOmAwQLnZnLGXVKUzeKQXIAw= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= diff --git a/modkit/kernel/bootstrap.go b/modkit/kernel/bootstrap.go index e58fedb..2eb2290 100644 --- a/modkit/kernel/bootstrap.go +++ b/modkit/kernel/bootstrap.go @@ -91,13 +91,12 @@ func (a *App) Closers() []io.Closer { return a.container.closersInBuildOrder() } -// Close calls Close on all io.Closer providers in reverse build order. +// Close closes providers implementing io.Closer in reverse build order. func (a *App) Close() error { return a.CloseContext(context.Background()) } -// CloseContext calls Close on all io.Closer providers in reverse build order, -// stopping early if the context is canceled. +// CloseContext closes providers implementing io.Closer in reverse build order. func (a *App) CloseContext(ctx context.Context) error { if a.closed.Load() { return nil diff --git a/modkit/kernel/bootstrap_test.go b/modkit/kernel/bootstrap_test.go index ced77f0..90abf29 100644 --- a/modkit/kernel/bootstrap_test.go +++ b/modkit/kernel/bootstrap_test.go @@ -109,6 +109,34 @@ func TestBootstrapAllowsReExportedTokens(t *testing.T) { } } +func TestBootstrapRejectsReExportOfNonExportedToken(t *testing.T) { + token := module.Token("hidden") + + modB := mod("B", nil, + []module.ProviderDef{{ + Token: token, + Build: func(_ module.Resolver) (any, error) { return "value", nil }, + }}, + nil, + nil, + ) + + modA := mod("A", []module.Module{modB}, nil, nil, []module.Token{token}) + + _, err := kernel.Bootstrap(modA) + if err == nil { + t.Fatalf("expected export validation error") + } + + var exportErr *kernel.ExportNotVisibleError + if !errors.As(err, &exportErr) { + t.Fatalf("unexpected error type: %T", err) + } + if exportErr.Module != "A" || exportErr.Token != token { + t.Fatalf("unexpected error: %v", err) + } +} + func TestBootstrapRejectsDuplicateProviderTokens(t *testing.T) { shared := module.Token("shared")