From d00172ae822179b996b302c68dbcfd75751aef63 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Tue, 20 Feb 2024 09:41:08 -0800 Subject: [PATCH 1/3] feat(fxtest): Add WithTestLogger option (#1159) fxtest.New has this really nice behavior where by default, it uses the `testing.TB` as the destination for log output. Unfortunately, fxtest is not good for testing failure cases because it fails the test if the container failed. app := fxtest.New(t, fx.Invoke(func() error { return errors.New("fail") }), ) err := app.Start(ctx) // We never get here because fxtest.New has already failed the test. So the expectation there is to use `fx.New(..)` which, by default, logs to stderr. This can be addressed by using the `fx.WithLogger` option in combination with `fxtest.NewTestLogger`, but it ends up being a mouthful: app := fx.New( fx.Invoke(func() error { return errors.New("fail") }), fx.WithLogger(func() fxevent.Logger { return fxtest.NewTestLogger(t) }), ) This PR is a proposal and implementation of a new `fxtest.WithTestLogger` option that shortens the above to: app := fx.New( fx.Invoke(func() error { return errors.New("fail") }), fxtest.WithTestLogger(t), ) As an example, a couple tests in Fx itself become more readable with this new API. Co-authored-by: Jacob Oaks --- CHANGELOG.md | 7 +++++-- app_test.go | 4 ++-- fxtest/app.go | 3 +-- fxtest/printer.go | 8 ++++++++ 4 files changed, 16 insertions(+), 6 deletions(-) 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/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/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 } From c013ff6d5e240d733cddfcbc372d46e10e79ac0b Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Tue, 20 Feb 2024 11:56:20 -0800 Subject: [PATCH 2/3] Move advanced features to package documentation (#1163) The documentation for parameter structs, result structs, named values, optional values, value groups, etc. is currently scattered between In and Out. First, on the logistics end: this has made it impossible to link to some of these docs because they share the same hadings: https://pkg.go.dev/go.uber.org/fx#hdr-Named_Values goes to the named values section of fx.In. It's not possible to link to the named values section of the fx.Out struct (which is also called Named Values). More importantly, from the doc readability point of view, jumping between In and Out is annoying and doesn't make for good reading. This PR moves these to the package-level docs and improves the flow of how this information is presented: First, we introduce parameter and result structs. Then we weave through the two introducing various topics: Producing named values Result structs Consuming named values Parameter structs Optional dependencies Parameter structs Producing value groups Result structs Consuming value groups Parameter structs Soft value groups Parameter structs Value group flattening Result structs Unexported fields Parameter structs This makes the story of these topics a bit easier to follow. In the future, we can introduce annotation right after parameter and result structs, or between value groups and soft value groups. Note that this does not touch on the core operations (provide, invoke, decorate). It's just moving existing documentation around. --- doc.go | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++- inout.go | 353 +++---------------------------------------------------- 2 files changed, 340 insertions(+), 340 deletions(-) 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/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 From d4cf3334fdb3cc793350c1afd005d60c58d860a7 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Tue, 20 Feb 2024 12:01:18 -0800 Subject: [PATCH 3/3] app.go: Take a pass over the godoc (#1162) The godocs for most of the public API in app.go hasn't been touched in a while. I took a quick pass over it to adjust it better to current recommendations and best practices. This also makes more use of Go's `[..]` linking. One notable change: Instead of Options, logical groups of options are now recomended via Module. --------- Co-authored-by: Jacob Oaks --- annotated.go | 27 +++++++--- app.go | 141 ++++++++++++++++++++++++++++----------------------- module.go | 36 ++++++++++++- 3 files changed, 131 insertions(+), 73 deletions(-) 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/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,