diff --git a/CHANGELOG.md b/CHANGELOG.md index b30c21c45..d85328233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unrelease -- No changes yet. +## Unreleased + +### Added +- fxtest: Add WithTestLogger option that uses a `testing.TB` as the + Fx event logger. ## [1.20.1](https://github.com/uber-go/fx/compare/v1.20.0...v1.20.1) - 2023-10-17 diff --git a/annotated.go b/annotated.go index 89f778783..4e0238661 100644 --- a/annotated.go +++ b/annotated.go @@ -56,8 +56,10 @@ import ( // }) // // Annotated cannot be used with constructors which produce fx.Out objects. +// When used with [Supply], Target is a value instead of a constructor. // -// When used with fx.Supply, the target is a value rather than a constructor function. +// This type represents a less powerful version of the [Annotate] construct; +// prefer [Annotate] where possible. type Annotated struct { // If specified, this will be used as the name for all non-error values returned // by the constructor. For more information on named values, see the documentation @@ -109,8 +111,9 @@ var ( } ) -// Annotation can be passed to Annotate(f interface{}, anns ...Annotation) -// for annotating the parameter and result types of a function. +// Annotation specifies how to wrap a target for [Annotate]. +// It can be used to set up additional options for a constructor, +// or with [Supply], for a value. type Annotation interface { apply(*annotated) error build(*annotated) (interface{}, error) @@ -334,8 +337,15 @@ func (pt paramTagsAnnotation) parameters(ann *annotated) ( } // ParamTags is an Annotation that annotates the parameter(s) of a function. +// // When multiple tags are specified, each tag is mapped to the corresponding // positional parameter. +// For example, the following will refer to a named database connection, +// and the default, unnamed logger: +// +// fx.Annotate(func(log *log.Logger, conn *sql.DB) *Handler { +// // ... +// }, fx.ParamTags("", `name:"ro"`)) // // ParamTags cannot be used in a function that takes an fx.In struct as a // parameter. @@ -522,6 +532,12 @@ func (rt resultTagsAnnotation) results(ann *annotated) ( // When multiple tags are specified, each tag is mapped to the corresponding // positional result. // +// For example, the following will produce a named database connection. +// +// fx.Annotate(func() (*sql.DB, error) { +// // ... +// }, fx.ResultTags(`name:"ro"`)) +// // ResultTags cannot be used on a function that returns an fx.Out struct. func ResultTags(tags ...string) Annotation { return resultTagsAnnotation{tags} @@ -1702,9 +1718,6 @@ func (ann *annotated) currentParamTypes() []reflect.Type { // ) // ) // -// is considered an invalid usage and will not apply any of the -// Annotations to NewGateway. -// // If more tags are given than the number of parameters/results, only // the ones up to the number of parameters/results will be applied. // @@ -1732,7 +1745,7 @@ func (ann *annotated) currentParamTypes() []reflect.Type { // // If we provide the above to the application, // any constructor in the Fx application can inject its HTTP handlers -// by using fx.Annotate, fx.Annotated, or fx.Out. +// by using [Annotate], [Annotated], or [Out]. // // fx.Annotate( // func(..) http.Handler { ... }, diff --git a/app.go b/app.go index 728b07cb0..8ede19eb5 100644 --- a/app.go +++ b/app.go @@ -40,13 +40,19 @@ import ( ) // DefaultTimeout is the default timeout for starting or stopping an -// application. It can be configured with the StartTimeout and StopTimeout +// application. It can be configured with the [StartTimeout] and [StopTimeout] // options. const DefaultTimeout = 15 * time.Second -// An Option configures an App using the functional options paradigm -// popularized by Rob Pike. If you're unfamiliar with this style, see -// https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html. +// An Option specifies the behavior of the application. +// This is the primary means by which you interface with Fx. +// +// Zero or more options are specified at startup with [New]. +// Options cannot be changed once an application has been initialized. +// Options may be grouped into a single option using the [Options] function. +// A group of options providing a logical unit of functionality +// may use [Module] to name that functionality +// and scope certain operations to within that module. type Option interface { fmt.Stringer @@ -73,37 +79,17 @@ func (errs errorOption) String() string { return fmt.Sprintf("fx.Error(%v)", multierr.Combine(errs...)) } -// Options converts a collection of Options into a single Option. This allows -// packages to bundle sophisticated functionality into easy-to-use Fx modules. -// For example, a logging package might export a simple option like this: -// -// package logging -// -// var Module = fx.Provide(func() *log.Logger { -// return log.New(os.Stdout, "", 0) -// }) -// -// A shared all-in-one microservice package could then use Options to bundle -// logging with similar metrics, tracing, and gRPC modules: +// Options bundles a group of options together into a single option. // -// package server +// Use Options to group together options that don't belong in a [Module]. // -// var Module = fx.Options( -// logging.Module, -// metrics.Module, -// tracing.Module, -// grpc.Module, +// var loggingAndMetrics = fx.Options( +// logging.Module, +// metrics.Module, +// fx.Invoke(func(logger *log.Logger) { +// app.globalLogger = logger +// }), // ) -// -// Since this all-in-one module has a minimal API surface, it's easy to add -// new functionality to it without breaking existing users. Individual -// applications can take advantage of all this functionality with only one -// line of code: -// -// app := fx.New(server.Module) -// -// Use this pattern sparingly, since it limits the user's ability to customize -// their application. func Options(opts ...Option) Option { return optionGroup(opts) } @@ -125,6 +111,10 @@ func (og optionGroup) String() string { } // StartTimeout changes the application's start timeout. +// This controls the total time that all [OnStart] hooks have to complete. +// If the timeout is exceeded, the application will fail to start. +// +// Defaults to [DefaultTimeout]. func StartTimeout(v time.Duration) Option { return startTimeoutOption(v) } @@ -145,6 +135,10 @@ func (t startTimeoutOption) String() string { } // StopTimeout changes the application's stop timeout. +// This controls the total time that all [OnStop] hooks have to complete. +// If the timeout is exceeded, the application will exit early. +// +// Defaults to [DefaultTimeout]. func StopTimeout(v time.Duration) Option { return stopTimeoutOption(v) } @@ -186,18 +180,27 @@ func (o recoverFromPanicsOption) String() string { return "fx.RecoverFromPanics()" } -// WithLogger specifies how Fx should build an fxevent.Logger to log its events -// to. The argument must be a constructor with one of the following return -// types. +// WithLogger specifies the [fxevent.Logger] used by Fx to log its own events +// (e.g. a constructor was provided, a function was invoked, etc.). +// +// The argument to this is a constructor with one of the following return +// types: // // fxevent.Logger // (fxevent.Logger, error) // +// The constructor may depend on any other types provided to the application. // For example, // // WithLogger(func(logger *zap.Logger) fxevent.Logger { // return &fxevent.ZapLogger{Logger: logger} // }) +// +// If specified, Fx will construct the logger and log all its events to the +// specified logger. +// +// If Fx fails to build the logger, or no logger is specified, it will fall back to +// [fxevent.ConsoleLogger] configured to write to stderr. func WithLogger(constructor interface{}) Option { return withLoggerOption{ constructor: constructor, @@ -224,14 +227,15 @@ func (l withLoggerOption) String() string { // Printer is the interface required by Fx's logging backend. It's implemented // by most loggers, including the one bundled with the standard library. // -// Note, this will be deprecate with next release and you will need to implement -// fxevent.Logger interface instead. +// Note, this will be deprecated in a future release. +// Prefer to use [fxevent.Logger] instead. type Printer interface { Printf(string, ...interface{}) } // Logger redirects the application's log output to the provided printer. -// Deprecated: use WithLogger instead. +// +// Prefer to use [WithLogger] instead. func Logger(p Printer) Option { return loggerOption{p} } @@ -252,8 +256,11 @@ func (l loggerOption) String() string { return fmt.Sprintf("fx.Logger(%v)", l.p) } -// NopLogger disables the application's log output. Note that this makes some -// failures difficult to debug, since no errors are printed to console. +// NopLogger disables the application's log output. +// +// Note that this makes some failures difficult to debug, +// since no errors are printed to console. +// Prefer to log to an in-memory buffer instead. var NopLogger = WithLogger(func() fxevent.Logger { return fxevent.NopLogger }) // An App is a modular application built around dependency injection. Most @@ -261,11 +268,11 @@ var NopLogger = WithLogger(func() fxevent.Logger { return fxevent.NopLogger }) // convenience method. In more unusual cases, users may need to use the Err, // Start, Done, and Stop methods by hand instead of relying on Run. // -// New creates and initializes an App. All applications begin with a +// [New] creates and initializes an App. All applications begin with a // constructor for the Lifecycle type already registered. // // In addition to that built-in functionality, users typically pass a handful -// of Provide options and one or more Invoke options. The Provide options +// of [Provide] options and one or more [Invoke] options. The Provide options // teach the application how to instantiate a variety of types, and the Invoke // options describe how to initialize the application. // @@ -286,7 +293,7 @@ var NopLogger = WithLogger(func() fxevent.Logger { return fxevent.NopLogger }) // // At this point, the application has successfully started up. If started via // Run, it will continue operating until it receives a shutdown signal from -// Done (see the Done documentation for details); if started explicitly via +// Done (see the [App.Done] documentation for details); if started explicitly via // Start, it will operate until the user calls Stop. On shutdown, OnStop hooks // execute one at a time, in reverse order, and must all complete within a // configurable deadline (again, 15 seconds by default). @@ -340,6 +347,10 @@ type invoke struct { } // ErrorHandler handles Fx application startup errors. +// Register these with [ErrorHook]. +// If specified, and the application fails to start up, +// the failure will still cause a crash, +// but you'll have a chance to log the error or take some other action. type ErrorHandler interface { HandleError(error) } @@ -407,7 +418,7 @@ func ValidateApp(opts ...Option) error { } // New creates and initializes an App, immediately executing any functions -// registered via Invoke options. See the documentation of the App struct for +// registered via [Invoke] options. See the documentation of the App struct for // details on the application's initialization, startup, and shutdown logic. func New(opts ...Option) *App { logger := fxlog.DefaultLogger(os.Stderr) @@ -526,6 +537,8 @@ func (app *App) log() fxevent.Logger { // initialization. On failure to build the dependency graph, it is attached // to the error and if possible, colorized to highlight the root cause of the // failure. +// +// Note that DotGraph does not yet recognize [Decorate] and [Replace]. type DotGraph string type errWithGraph interface { @@ -546,6 +559,8 @@ func (err errorWithGraph) Error() string { } // VisualizeError returns the visualization of the error if available. +// +// Note that VisualizeError does not yet recognize [Decorate] and [Replace]. func VisualizeError(err error) (string, error) { var erg errWithGraph if errors.As(err, &erg) { @@ -566,12 +581,15 @@ func (app *App) exit(code int) { } // Run starts the application, blocks on the signals channel, and then -// gracefully shuts the application down. It uses DefaultTimeout to set a +// gracefully shuts the application down. It uses [DefaultTimeout] to set a // deadline for application startup and shutdown, unless the user has -// configured different timeouts with the StartTimeout or StopTimeout options. +// configured different timeouts with the [StartTimeout] or [StopTimeout] options. // It's designed to make typical applications simple to run. +// The minimal Fx application looks like this: +// +// fx.New().Run() // -// However, all of Run's functionality is implemented in terms of the exported +// All of Run's functionality is implemented in terms of the exported // Start, Done, and Stop methods. Applications with more specialized needs // can use those methods directly instead of relying on Run. func (app *App) Run() { @@ -719,36 +737,31 @@ func (app *App) Stop(ctx context.Context) (err error) { // the same terminal as the running process. // // Alternatively, a signal can be broadcast to all done channels manually by -// using the Shutdown functionality (see the Shutdowner documentation for details). -// -// Note: The channel Done returns will not receive a signal unless the application -// as been started via Start or Run. +// using the Shutdown functionality (see the [Shutdowner] documentation for details). func (app *App) Done() <-chan os.Signal { return app.receivers.Done() } // Wait returns a channel of [ShutdownSignal] to block on after starting the -// application and function, similar to [App.Done], but with a minor difference. -// Should an ExitCode be provided as a [ShutdownOption] to -// the Shutdowner Shutdown method, the exit code will be available as part -// of the ShutdownSignal struct. -// -// Should the app receive a SIGTERM or SIGINT, the given -// signal will be populated in the ShutdownSignal struct. +// application and function, similar to [App.Done], but with a minor difference: +// if the app was shut down via [Shutdowner.Shutdown], +// the exit code (if provied via [ExitCode]) will be available +// in the [ShutdownSignal] struct. +// Otherwise, the signal that was received will be set. func (app *App) Wait() <-chan ShutdownSignal { return app.receivers.Wait() } -// StartTimeout returns the configured startup timeout. Apps default to using -// DefaultTimeout, but users can configure this behavior using the -// StartTimeout option. +// StartTimeout returns the configured startup timeout. +// This defaults to [DefaultTimeout], and can be changed with the +// [StartTimeout] option. func (app *App) StartTimeout() time.Duration { return app.startTimeout } -// StopTimeout returns the configured shutdown timeout. Apps default to using -// DefaultTimeout, but users can configure this behavior using the StopTimeout -// option. +// StopTimeout returns the configured shutdown timeout. +// This defaults to [DefaultTimeout], and can be changed with the +// [StopTimeout] option. func (app *App) StopTimeout() time.Duration { return app.stopTimeout } diff --git a/app_test.go b/app_test.go index a45b4cd37..2ac6ef2bf 100644 --- a/app_test.go +++ b/app_test.go @@ -53,7 +53,7 @@ func NewForTest(tb testing.TB, opts ...Option) *App { // Provide both: Logger and WithLogger so that if the test // WithLogger fails, we don't pollute stderr. Logger(fxtest.NewTestPrinter(tb)), - WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(tb) }), + fxtest.WithTestLogger(tb), } opts = append(testOpts, opts...) @@ -73,7 +73,7 @@ func validateTestApp(tb testing.TB, opts ...Option) error { // Provide both: Logger and WithLogger so that if the test // WithLogger fails, we don't pollute stderr. Logger(fxtest.NewTestPrinter(tb)), - WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(tb) }), + fxtest.WithTestLogger(tb), } opts = append(testOpts, opts...) diff --git a/doc.go b/doc.go index 590fe8d39..4ee2227ac 100644 --- a/doc.go +++ b/doc.go @@ -27,13 +27,334 @@ // to use struct tags or embed special types, so Fx automatically works well // with most Go packages. // -// Basic usage is explained in the package-level example below. If you're new -// to Fx, start there! Advanced features, including named instances, optional -// parameters, and value groups, are explained under the In and Out types. +// # Basic usage +// +// Basic usage is explained in the package-level example. +// If you're new to Fx, start there! +// +// Advanced features, including named instances, optional parameters, +// and value groups, are explained in this section further down. // // # Testing Fx Applications // // To test functions that use the Lifecycle type or to write end-to-end tests // of your Fx application, use the helper functions and types provided by the // go.uber.org/fx/fxtest package. +// +// # Parameter Structs +// +// Fx constructors declare their dependencies as function parameters. This can +// quickly become unreadable if the constructor has a lot of dependencies. +// +// func NewHandler(users *UserGateway, comments *CommentGateway, posts *PostGateway, votes *VoteGateway, authz *AuthZGateway) *Handler { +// // ... +// } +// +// To improve the readability of constructors like this, create a struct that +// lists all the dependencies as fields and change the function to accept that +// struct instead. The new struct is called a parameter struct. +// +// Fx has first class support for parameter structs: any struct embedding +// fx.In gets treated as a parameter struct, so the individual fields in the +// struct are supplied via dependency injection. Using a parameter struct, we +// can make the constructor above much more readable: +// +// type HandlerParams struct { +// fx.In +// +// Users *UserGateway +// Comments *CommentGateway +// Posts *PostGateway +// Votes *VoteGateway +// AuthZ *AuthZGateway +// } +// +// func NewHandler(p HandlerParams) *Handler { +// // ... +// } +// +// Though it's rarelly necessary to mix the two, constructors can receive any +// combination of parameter structs and parameters. +// +// func NewHandler(p HandlerParams, l *log.Logger) *Handler { +// // ... +// } +// +// # Result Structs +// +// Result structs are the inverse of parameter structs. +// These structs represent multiple outputs from a +// single function as fields. Fx treats all structs embedding fx.Out as result +// structs, so other constructors can rely on the result struct's fields +// directly. +// +// Without result structs, we sometimes have function definitions like this: +// +// func SetupGateways(conn *sql.DB) (*UserGateway, *CommentGateway, *PostGateway, error) { +// // ... +// } +// +// With result structs, we can make this both more readable and easier to +// modify in the future: +// +// type Gateways struct { +// fx.Out +// +// Users *UserGateway +// Comments *CommentGateway +// Posts *PostGateway +// } +// +// func SetupGateways(conn *sql.DB) (Gateways, error) { +// // ... +// } +// +// # Named Values +// +// Some use cases require the application container to hold multiple values of +// the same type. +// +// A constructor that produces a result struct can tag any field with +// `name:".."` to have the corresponding value added to the graph under the +// specified name. An application may contain at most one unnamed value of a +// given type, but may contain any number of named values of the same type. +// +// type ConnectionResult struct { +// fx.Out +// +// ReadWrite *sql.DB `name:"rw"` +// ReadOnly *sql.DB `name:"ro"` +// } +// +// func ConnectToDatabase(...) (ConnectionResult, error) { +// // ... +// return ConnectionResult{ReadWrite: rw, ReadOnly: ro}, nil +// } +// +// Similarly, a constructor that accepts a parameter struct can tag any field +// with `name:".."` to have the corresponding value injected by name. +// +// type GatewayParams struct { +// fx.In +// +// WriteToConn *sql.DB `name:"rw"` +// ReadFromConn *sql.DB `name:"ro"` +// } +// +// func NewCommentGateway(p GatewayParams) (*CommentGateway, error) { +// // ... +// } +// +// Note that both the name AND type of the fields on the +// parameter struct must match the corresponding result struct. +// +// # Optional Dependencies +// +// Constructors often have optional dependencies on some types: if those types are +// missing, they can operate in a degraded state. Fx supports optional +// dependencies via the `optional:"true"` tag to fields on parameter structs. +// +// type UserGatewayParams struct { +// fx.In +// +// Conn *sql.DB +// Cache *redis.Client `optional:"true"` +// } +// +// If an optional field isn't available in the container, the constructor +// receives the field's zero value. +// +// func NewUserGateway(p UserGatewayParams, log *log.Logger) (*UserGateway, error) { +// if p.Cache == nil { +// log.Print("Caching disabled") +// } +// // ... +// } +// +// Constructors that declare optional dependencies MUST gracefully handle +// situations in which those dependencies are absent. +// +// The optional tag also allows adding new dependencies without breaking +// existing consumers of the constructor. +// +// The optional tag may be combined with the name tag to declare a named +// value dependency optional. +// +// type GatewayParams struct { +// fx.In +// +// WriteToConn *sql.DB `name:"rw"` +// ReadFromConn *sql.DB `name:"ro" optional:"true"` +// } +// +// func NewCommentGateway(p GatewayParams, log *log.Logger) (*CommentGateway, error) { +// if p.ReadFromConn == nil { +// log.Print("Warning: Using RW connection for reads") +// p.ReadFromConn = p.WriteToConn +// } +// // ... +// } +// +// # Value Groups +// +// To make it easier to produce and consume many values of the same type, Fx +// supports named, unordered collections called value groups. +// +// Constructors can send values into value groups by returning a result struct +// tagged with `group:".."`. +// +// type HandlerResult struct { +// fx.Out +// +// Handler Handler `group:"server"` +// } +// +// func NewHelloHandler() HandlerResult { +// // ... +// } +// +// func NewEchoHandler() HandlerResult { +// // ... +// } +// +// Any number of constructors may provide values to this named collection, but +// the ordering of the final collection is unspecified. +// +// Value groups require parameter and result structs to use fields with +// different types: if a group of constructors each returns type T, parameter +// structs consuming the group must use a field of type []T. +// +// Parameter structs can request a value group by using a field of type []T +// tagged with `group:".."`. +// This will execute all constructors that provide a value to +// that group in an unspecified order, then collect all the results into a +// single slice. +// +// type ServerParams struct { +// fx.In +// +// Handlers []Handler `group:"server"` +// } +// +// func NewServer(p ServerParams) *Server { +// server := newServer() +// for _, h := range p.Handlers { +// server.Register(h) +// } +// return server +// } +// +// Note that values in a value group are unordered. Fx makes no guarantees +// about the order in which these values will be produced. +// +// # Soft Value Groups +// +// By default, when a constructor declares a dependency on a value group, +// all values provided to that value group are eagerly instantiated. +// That is undesirable for cases where an optional component wants to +// constribute to a value group, but only if it was actually used +// by the rest of the application. +// +// A soft value group can be thought of as a best-attempt at populating the +// group with values from constructors that have already run. In other words, +// if a constructor's output type is only consumed by a soft value group, +// it will not be run. +// +// Note that Fx randomizes the order of values in the value group, +// so the slice of values may not match the order in which constructors +// were run. +// +// To declare a soft relationship between a group and its constructors, use +// the `soft` option on the input group tag (`group:"[groupname],soft"`). +// This option is only valid for input parameters. +// +// type Params struct { +// fx.In +// +// Handlers []Handler `group:"server,soft"` +// Logger *zap.Logger +// } +// +// func NewServer(p Params) *Server { +// // ... +// } +// +// With such a declaration, a constructor that provides a value to the 'server' +// value group will be called only if there's another instantiated component +// that consumes the results of that constructor. +// +// func NewHandlerAndLogger() (Handler, *zap.Logger) { +// // ... +// } +// +// func NewHandler() Handler { +// // ... +// } +// +// fx.Provide( +// fx.Annotate(NewHandlerAndLogger, fx.ResultTags(`group:"server"`)), +// fx.Annotate(NewHandler, fx.ResultTags(`group:"server"`)), +// ) +// +// NewHandlerAndLogger will be called because the Logger is consumed by the +// application, but NewHandler will not be called because it's only consumed +// by the soft value group. +// +// # Value group flattening +// +// By default, values of type T produced to a value group are consumed as []T. +// +// type HandlerResult struct { +// fx.Out +// +// Handler Handler `group:"server"` +// } +// +// type ServerParams struct { +// fx.In +// +// Handlers []Handler `group:"server"` +// } +// +// This means that if the producer produces []T, +// the consumer must consume [][]T. +// +// There are cases where it's desirable +// for the producer (the fx.Out) to produce multiple values ([]T), +// and for the consumer (the fx.In) consume them as a single slice ([]T). +// Fx offers flattened value groups for this purpose. +// +// To provide multiple values for a group from a result struct, produce a +// slice and use the `,flatten` option on the group tag. This indicates that +// each element in the slice should be injected into the group individually. +// +// type HandlerResult struct { +// fx.Out +// +// Handler []Handler `group:"server,flatten"` +// // Consumed as []Handler in ServerParams. +// } +// +// # Unexported fields +// +// By default, a type that embeds fx.In may not have any unexported fields. The +// following will return an error if used with Fx. +// +// type Params struct { +// fx.In +// +// Logger *zap.Logger +// mu sync.Mutex +// } +// +// If you have need of unexported fields on such a type, you may opt-into +// ignoring unexported fields by adding the ignore-unexported struct tag to the +// fx.In. For example, +// +// type Params struct { +// fx.In `ignore-unexported:"true"` +// +// Logger *zap.Logger +// mu sync.Mutex +// } package fx // import "go.uber.org/fx" diff --git a/fxtest/app.go b/fxtest/app.go index 9ce2e8b1f..427f95422 100644 --- a/fxtest/app.go +++ b/fxtest/app.go @@ -24,7 +24,6 @@ import ( "context" "go.uber.org/fx" - "go.uber.org/fx/fxevent" ) // App is a wrapper around fx.App that provides some testing helpers. By @@ -38,7 +37,7 @@ type App struct { // New creates a new test application. func New(tb TB, opts ...fx.Option) *App { allOpts := make([]fx.Option, 0, len(opts)+1) - allOpts = append(allOpts, fx.WithLogger(func() fxevent.Logger { return NewTestLogger(tb) })) + allOpts = append(allOpts, WithTestLogger(tb)) allOpts = append(allOpts, opts...) app := fx.New(allOpts...) diff --git a/fxtest/printer.go b/fxtest/printer.go index 73f0f8f74..c16c155b6 100644 --- a/fxtest/printer.go +++ b/fxtest/printer.go @@ -32,6 +32,14 @@ func NewTestLogger(t TB) fxevent.Logger { return fxlog.DefaultLogger(testutil.WriteSyncer{T: t}) } +// WithTestLogger returns an fx.Option that uses the provided TB +// as the destination for Fx's log output. +func WithTestLogger(t TB) fx.Option { + return fx.WithLogger(func() fxevent.Logger { + return NewTestLogger(t) + }) +} + type testPrinter struct { TB } diff --git a/inout.go b/inout.go index 05303944c..e40ab8af6 100644 --- a/inout.go +++ b/inout.go @@ -22,343 +22,22 @@ package fx import "go.uber.org/dig" -// In can be embedded in a constructor's parameter struct to take advantage of -// advanced dependency injection features. -// -// Modules should take a single parameter struct that embeds an In in order to -// provide a forward-compatible API: since adding fields to a struct is -// backward-compatible, modules can then add optional dependencies in minor -// releases. -// -// # Parameter Structs -// -// Fx constructors declare their dependencies as function parameters. This can -// quickly become unreadable if the constructor has a lot of dependencies. -// -// func NewHandler(users *UserGateway, comments *CommentGateway, posts *PostGateway, votes *VoteGateway, authz *AuthZGateway) *Handler { -// // ... -// } -// -// To improve the readability of constructors like this, create a struct that -// lists all the dependencies as fields and change the function to accept that -// struct instead. The new struct is called a parameter struct. -// -// Fx has first class support for parameter structs: any struct embedding -// fx.In gets treated as a parameter struct, so the individual fields in the -// struct are supplied via dependency injection. Using a parameter struct, we -// can make the constructor above much more readable: -// -// type HandlerParams struct { -// fx.In -// -// Users *UserGateway -// Comments *CommentGateway -// Posts *PostGateway -// Votes *VoteGateway -// AuthZ *AuthZGateway -// } -// -// func NewHandler(p HandlerParams) *Handler { -// // ... -// } -// -// Though it's rarely a good idea, constructors can receive any combination of -// parameter structs and parameters. -// -// func NewHandler(p HandlerParams, l *log.Logger) *Handler { -// // ... -// } -// -// # Optional Dependencies -// -// Constructors often have optional dependencies on some types: if those types are -// missing, they can operate in a degraded state. Fx supports optional -// dependencies via the `optional:"true"` tag to fields on parameter structs. -// -// type UserGatewayParams struct { -// fx.In -// -// Conn *sql.DB -// Cache *redis.Client `optional:"true"` -// } -// -// If an optional field isn't available in the container, the constructor -// receives the field's zero value. -// -// func NewUserGateway(p UserGatewayParams, log *log.Logger) (*UserGateway, error) { -// if p.Cache == nil { -// log.Print("Caching disabled") -// } -// // ... -// } -// -// Constructors that declare optional dependencies MUST gracefully handle -// situations in which those dependencies are absent. -// -// The optional tag also allows adding new dependencies without breaking -// existing consumers of the constructor. -// -// # Named Values -// -// Some use cases require the application container to hold multiple values of -// the same type. For details on producing named values, see the documentation -// for the Out type. -// -// Fx allows functions to consume named values via the `name:".."` tag on -// parameter structs. Note that both the name AND type of the fields on the -// parameter struct must match the corresponding result struct. -// -// type GatewayParams struct { -// fx.In -// -// WriteToConn *sql.DB `name:"rw"` -// ReadFromConn *sql.DB `name:"ro"` -// } -// -// The name tag may be combined with the optional tag to declare the -// dependency optional. -// -// type GatewayParams struct { -// fx.In -// -// WriteToConn *sql.DB `name:"rw"` -// ReadFromConn *sql.DB `name:"ro" optional:"true"` -// } -// -// func NewCommentGateway(p GatewayParams, log *log.Logger) (*CommentGateway, error) { -// if p.ReadFromConn == nil { -// log.Print("Warning: Using RW connection for reads") -// p.ReadFromConn = p.WriteToConn -// } -// // ... -// } -// -// # Value Groups -// -// To make it easier to produce and consume many values of the same type, Fx -// supports named, unordered collections called value groups. For details on -// producing value groups, see the documentation for the Out type. -// -// Functions can depend on a value group by requesting a slice tagged with -// `group:".."`. This will execute all constructors that provide a value to -// that group in an unspecified order, then collect all the results into a -// single slice. Keep in mind that this makes the types of the parameter and -// result struct fields different: if a group of constructors each returns -// type T, parameter structs consuming the group must use a field of type []T. -// -// type ServerParams struct { -// fx.In -// -// Handlers []Handler `group:"server"` -// } -// -// func NewServer(p ServerParams) *Server { -// server := newServer() -// for _, h := range p.Handlers { -// server.Register(h) -// } -// return server -// } -// -// Note that values in a value group are unordered. Fx makes no guarantees -// about the order in which these values will be produced. -// -// # Soft Value Groups -// -// A soft value group can be thought of as a best-attempt at populating the -// group with values from constructors that have already run. In other words, -// if a constructor's output type is only consumed by a soft value group, -// it will not be run. -// -// Note that Fx does not guarantee precise execution order of constructors -// or invokers, which means that the change in code that affects execution -// ordering of other constructors or functions will affect the values -// populated in this group. -// -// To declare a soft relationship between a group and its constructors, use -// the `soft` option on the group tag (`group:"[groupname],soft"`). -// This option is only valid for input parameters. -// -// type Params struct { -// fx.In -// -// Handlers []Handler `group:"server,soft"` -// Logger *zap.Logger -// } -// -// NewHandlerAndLogger := func() (Handler, *zap.Logger) { ... } -// NewHandler := func() Handler { ... } -// Foo := func(Params) { ... } -// -// app := fx.New( -// fx.Provide(fx.Annotate(NewHandlerAndLogger, fx.ResultTags(`group:"server"`))), -// fx.Provide(fx.Annotate(NewHandler, fx.ResultTags(`group::"server"`))), -// fx.Invoke(Foo), -// ) -// -// The only constructor called is `NewHandlerAndLogger`, because this also provides -// `*zap.Logger` needed in the `Params` struct received by `Foo`. The Handlers -// group will be populated with a single Handler returned by `NewHandlerAndLogger`. -// -// In the next example, the slice `s` isn't populated as the provider would be -// called only because `strings` soft group value is its only consumer. -// -// app := fx.New( -// fx.Provide( -// fx.Annotate( -// func() (string, int) { return "hello", 42 }, -// fx.ResultTags(`group:"strings"`), -// ), -// ), -// fx.Invoke( -// fx.Annotate(func(s []string) { -// // s will be an empty slice -// }, fx.ParamTags(`group:"strings,soft"`)), -// ), -// ) -// -// In the next example, the slice `s` will be populated because there is a -// consumer for the same type which is not a `soft` dependency. -// -// app := fx.New( -// fx.Provide( -// fx.Annotate( -// func() string { "hello" }, -// fx.ResultTags(`group:"strings"`), -// ), -// ), -// fx.Invoke( -// fx.Annotate(func(b []string) { -// // b is []string{"hello"} -// }, fx.ParamTags(`group:"strings"`)), -// ), -// fx.Invoke( -// fx.Annotate(func(s []string) { -// // s is []string{"hello"} -// }, fx.ParamTags(`group:"strings,soft"`)), -// ), -// ) -// -// # Unexported fields -// -// By default, a type that embeds fx.In may not have any unexported fields. The -// following will return an error if used with Fx. -// -// type Params struct { -// fx.In -// -// Logger *zap.Logger -// mu sync.Mutex -// } -// -// If you have need of unexported fields on such a type, you may opt-into -// ignoring unexported fields by adding the ignore-unexported struct tag to the -// fx.In. For example, -// -// type Params struct { -// fx.In `ignore-unexported:"true"` -// -// Logger *zap.Logger -// mu sync.Mutex -// } +// In can be embedded into a struct to mark it as a parameter struct. +// This allows it to make use of advanced dependency injection features. +// See package documentation for more information. +// +// It's recommended that shared modules use a single parameter struct to +// provide a forward-compatible API: +// adding new optional fields to a struct is backward-compatible, +// so modules can evolve as needs change. type In = dig.In -// Out is the inverse of In: it can be embedded in result structs to take -// advantage of advanced features. -// -// Modules should return a single result struct that embeds an Out in order to -// provide a forward-compatible API: since adding fields to a struct is -// backward-compatible, minor releases can provide additional types. -// -// # Result Structs -// -// Result structs are the inverse of parameter structs (discussed in the In -// documentation). These structs represent multiple outputs from a -// single function as fields. Fx treats all structs embedding fx.Out as result -// structs, so other constructors can rely on the result struct's fields -// directly. -// -// Without result structs, we sometimes have function definitions like this: -// -// func SetupGateways(conn *sql.DB) (*UserGateway, *CommentGateway, *PostGateway, error) { -// // ... -// } -// -// With result structs, we can make this both more readable and easier to -// modify in the future: -// -// type Gateways struct { -// fx.Out -// -// Users *UserGateway -// Comments *CommentGateway -// Posts *PostGateway -// } -// -// func SetupGateways(conn *sql.DB) (Gateways, error) { -// // ... -// } -// -// # Named Values -// -// Some use cases require the application container to hold multiple values of -// the same type. For details on consuming named values, see the documentation -// for the In type. -// -// A constructor that produces a result struct can tag any field with -// `name:".."` to have the corresponding value added to the graph under the -// specified name. An application may contain at most one unnamed value of a -// given type, but may contain any number of named values of the same type. -// -// type ConnectionResult struct { -// fx.Out -// -// ReadWrite *sql.DB `name:"rw"` -// ReadOnly *sql.DB `name:"ro"` -// } -// -// func ConnectToDatabase(...) (ConnectionResult, error) { -// // ... -// return ConnectionResult{ReadWrite: rw, ReadOnly: ro}, nil -// } -// -// # Value Groups -// -// To make it easier to produce and consume many values of the same type, Fx -// supports named, unordered collections called value groups. For details on -// consuming value groups, see the documentation for the In type. -// -// Constructors can send values into value groups by returning a result struct -// tagged with `group:".."`. -// -// type HandlerResult struct { -// fx.Out -// -// Handler Handler `group:"server"` -// } -// -// func NewHelloHandler() HandlerResult { -// // ... -// } -// -// func NewEchoHandler() HandlerResult { -// // ... -// } -// -// Any number of constructors may provide values to this named collection, but -// the ordering of the final collection is unspecified. Keep in mind that -// value groups require parameter and result structs to use fields with -// different types: if a group of constructors each returns type T, parameter -// structs consuming the group must use a field of type []T. -// -// To provide multiple values for a group from a result struct, produce a -// slice and use the `,flatten` option on the group tag. This indicates that -// each element in the slice should be injected into the group individually. -// -// type IntResult struct { -// fx.Out -// -// Handler []int `group:"server"` // Consume as [][]int -// Handler []int `group:"server,flatten"` // Consume as []int -// } +// Out is the inverse of In: it marks a struct as a result struct so that +// it can be used with advanced dependency injection features. +// See package documentation for more information. +// +// It's recommended that shared modules use a single result struct to +// provide a forward-compatible API: +// adding new fields to a struct is backward-compatible, +// so modules can produce more outputs as they grow. type Out = dig.Out diff --git a/module.go b/module.go index bb4f74484..d0aa13404 100644 --- a/module.go +++ b/module.go @@ -41,8 +41,40 @@ type container interface { } // Module is a named group of zero or more fx.Options. -// A Module creates a scope in which certain operations are taken -// place. For more information, see [Decorate], [Replace], or [Invoke]. +// +// A Module scopes the effect of certain operations to within the module. +// For more information, see [Decorate], [Replace], or [Invoke]. +// +// Module allows packages to bundle sophisticated functionality into easy-to-use +// logical units. +// For example, a logging package might export a simple option like this: +// +// package logging +// +// var Module = fx.Module("logging", +// fx.Provide(func() *log.Logger { +// return log.New(os.Stdout, "", 0) +// }), +// // ... +// ) +// +// A shared all-in-one microservice package could use Module to bundle +// all required components of a microservice: +// +// package server +// +// var Module = fx.Module("server", +// logging.Module, +// metrics.Module, +// tracing.Module, +// rpc.Module, +// ) +// +// When new global functionality is added to the service ecosystem, +// it can be added to the shared module with minimal churn for users. +// +// Use the all-in-one pattern sparingly. +// It limits the flexibility available to the application. func Module(name string, opts ...Option) Option { mo := moduleOption{ name: name,