diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 8a7430d5a..1992c0fae 100755 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -74,6 +74,7 @@ module.exports = { title: 'Concepts', children: [ 'lifecycle.md', + 'modules.md', ], }, { diff --git a/docs/Makefile b/docs/Makefile index 3bf8c100b..d641d2347 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,7 @@ export PATH := $(shell pwd)/bin:$(PATH) MDOX = $(shell pwd)/../bin/mdox MDOX_FMT_FLAGS = --soft-wraps --links.validate --links.validate.config-file $(shell pwd)/.mdox-validate.yaml -MD_FILES = $(shell git ls-files '*.md') +MD_FILES ?= $(shell git ls-files '*.md') .PHONY: fmt: $(MDOX) diff --git a/docs/bin/region b/docs/bin/region index 08862f48f..0b05de3c4 100755 --- a/docs/bin/region +++ b/docs/bin/region @@ -2,7 +2,7 @@ set -euo pipefail IFS=$'\n\t' -if [[ $# -ne 2 ]]; then +if [[ $# -lt 2 ]]; then echo >&2 'USAGE: region FILE REGION1 REGION2 ...' echo >&2 echo >&2 'Extracts text from FILE marked by "// region" blocks.' diff --git a/docs/ex/modules/module.go b/docs/ex/modules/module.go new file mode 100644 index 000000000..ab78fa921 --- /dev/null +++ b/docs/ex/modules/module.go @@ -0,0 +1,108 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package modules + +import ( + "go.uber.org/fx" + "go.uber.org/zap" +) + +// Module is an example of an Fx module's skeleton. +// region start +// region provide +// region invoke +// region decorate +var Module = fx.Module("server", + // endregion start + fx.Provide( + New, + parseConfig, + ), + // endregion provide + fx.Invoke(startServer), + // endregion invoke + fx.Decorate(wrapLogger), + +// region provide +// region invoke +) + +// endregion invoke +// endregion provide +// endregion decorate + +// Config is the configuration of the server. +// region config +type Config struct { + Addr string `yaml:"addr"` +} + +// endregion config + +func parseConfig() (Config, error) { + return Config{}, nil +} + +// Params defines the parameters of the module. +// region params +type Params struct { + fx.In + + Log *zap.Logger + Config Config +} + +// endregion params + +// Result defines the results of the module. +// region result +type Result struct { + fx.Out + + Server *Server +} + +// endregion result + +// New builds a new server. +// region new +func New(p Params) (Result, error) { + // endregion new + return Result{ + Server: &Server{}, + }, nil +} + +// Server is the server. +type Server struct{} + +// Start starts the server. +func (*Server) Start() error { + return nil +} + +func startServer(srv *Server) error { + return srv.Start() +} + +func wrapLogger(log *zap.Logger) *zap.Logger { + return log.With(zap.String("component", "mymodule")) +} diff --git a/docs/ex/modules/module_test.go b/docs/ex/modules/module_test.go new file mode 100644 index 000000000..f4a19028b --- /dev/null +++ b/docs/ex/modules/module_test.go @@ -0,0 +1,41 @@ +// Copyright (c) 2022 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package modules + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" + "go.uber.org/zap" +) + +func TestModule(t *testing.T) { + var got *Server + app := fxtest.New(t, + Module, + fx.Supply(zap.NewNop()), + fx.Populate(&got), + ) + app.RequireStart().RequireStop() + assert.NotNil(t, got) +} diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 000000000..679b81fce --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,299 @@ +--- +sidebarDepth: 2 +--- + +# Modules + +An Fx module is a shareable Go library or package +that provides self-contained functionality to an Fx application. + +## Writing modules + +To write an Fx module: + +1. Define a top-level `Module` variable built from an `fx.Module` call. + Give your module a short, memorable name for logs. + + ```go mdox-exec='region ex/modules/module.go start' + var Module = fx.Module("server", + ``` + +2. Add components of your module with `fx.Provide`. + + ```go mdox-exec='region ex/modules/module.go provide' + var Module = fx.Module("server", + fx.Provide( + New, + parseConfig, + ), + ) + ``` + +3. If your module has a function that must always run, + add an `fx.Invoke` with it. + + ```go mdox-exec='region ex/modules/module.go invoke' + var Module = fx.Module("server", + fx.Provide( + New, + parseConfig, + ), + fx.Invoke(startServer), + ) + ``` + +4. Lastly, if your module needs to decorate its dependencies + before consuming them, add an `fx.Decorate` call for it. + + ```go mdox-exec='region ex/modules/module.go decorate' + var Module = fx.Module("server", + fx.Provide( + New, + parseConfig, + ), + fx.Invoke(startServer), + fx.Decorate(wrapLogger), + ) + ``` + +That's all there's to writing modules. +The rest of this section covers standards and conventions +we've established for writing Fx modules at Uber. + +### Naming + +#### Packages + +Standalone Fx modules, +i.e. those distributed as an independent library, +or those that have an independent Go package in a library, +should be named for the library they wrap +or the functionality they provide, +with an added "fx" suffix. + +| Bad | Good | +|--------------------|-------------------| +| `package mylib` | `package mylibfx` | +| `package httputil` | `package httpfx` | + +Fx modules that are part of another Go package, +or single-serving modules written for a specific application +may omit this suffix. + +#### Parameter and result objects + +Parameter and result object types should be named +after the function they're for, +by adding a `Params` or `Result` suffix to the function's name. + +**Exception**: If the function name begins with `New`, +strip the `New` prefix before adding the `Params` or `Result` suffix. + +| Function | Parameter object | Result object | +|----------|------------------|---------------| +| New | Params | Result | +| Run | RunParams | RunResult | +| NewFoo | FooParams | FooResult | + +### Export boundary functions + +Export functions which are used by your module +via `fx.Provide` or `fx.Invoke` +if that functionality would not be otherwise accessible. + +```go mdox-exec='region ex/modules/module.go provide config new' +var Module = fx.Module("server", + fx.Provide( + New, + parseConfig, + ), +) + +type Config struct { + Addr string `yaml:"addr"` +} + +func New(p Params) (Result, error) { +``` + +In this example, we don't export `parseConfig`, +because it's a trivial `yaml.Decode` that we don't need to expose, +but we still export `Config` so users can decode it themselves. + +**Rationale**: +It should be possible to use your Fx module without using Fx itself. +A user should be able to call the constructor directly +and get the same functionality that the module would have provided with Fx. +This is necessary for break-glass situations and partial migrations. + +::: details Bad: No way to build the server without Fx + +```go +var Module = fx.Module("server", + fx.Provide(newServer), +) + +func newServer(...) (*Server, error) +``` + +::: + +### Use parameter objects + +Functions exposed by a module should not +accept dependencies directly as parameters. +Instead, they should use a [parameter object](parameter-objects.md). + +```go mdox-exec='region ex/modules/module.go params new' +type Params struct { + fx.In + + Log *zap.Logger + Config Config +} + +func New(p Params) (Result, error) { +``` + +**Rationale**: +Modules will inevitably need to declare new dependencies. +By using parameter objects, we can +[add new optional dependencies](parameter-objects.md#adding-new-parameters) +in a backwards-compatible manner without changing the function signature. + +::: details Bad: Cannot add new parameters without breaking + +```go +func New(log *zap.Logger) (Result, error) +``` + +::: + +### Use result objects + +Functions exposed by a module should not +declare their results as regular return values. +Instead, they should use a [result object](result-objects.md). + +```go mdox-exec='region ex/modules/module.go result new' +type Result struct { + fx.Out + + Server *Server +} + +func New(p Params) (Result, error) { +``` + +**Rationale**: +Modules will inevitably need to return new results. +By using result objects, we can +[produce new results](result-objects.md#adding-new-results) +in a backwards-compatible manner without changing the function signature. + +::: details Bad: Cannot add new results without breaking + +```go +func New(Params) (*Server, error) +``` + +::: + +### Don't provide what you don't own + +Fx modules should provide only those types to the application +that are within their purview. +Modules should not provide values they happen to use to the application. +Nor should modules bundle other modules wholesale. + +**Rationale**: +This leaves consumers free to choose how and where your dependencies come from. +They can use the method you recommend (e.g., "include zapfx.Module"), +or build their own variant of that dependency. + +::: details Bad: Provides a dependency + +```go +package httpfx + +type Result struct { + fx.Out + + Client *http.Client + Logger *zap.Logger // BAD +} +``` + +::: + +::: details Bad: Bundles another module + +```go +package httpfx + +var Module = fx.Module("http", + fx.Provide(New), + zapfx.Module, // BAD +) +``` + +::: + +**Exception**: +Organization or team-level "kitchen sink" modules +that exists solely to bundle other modules +may ignore this rule. +For example, at Uber we define an `uberfx.Module` +that bundles several other independent modules. +Everything in this module is required by *all* services. + +### Keep independent modules thin + +Independent Fx modules--those with names ending with "fx" +rarely contain non-trivial business logic. +If an Fx module is inside a package +that contains significant business logic, +it should not have the "fx" suffix in its name. + +**Rationale**: +It should be possible for someone to migrate to or away from Fx, +without rewriting their business logic. + +::: details Good: Business logic consumes net/http.Client + +```go +package httpfx + +import "net/http" + +type Result struct { + fx.Out + + Client *http.Client +} +``` + +::: + +::: details Bad: Fx module implements logger + +```go +package logfx + +type Logger struct { + // ... +} + +func New(...) Logger +``` + +::: + +### Invoke sparingly + +Be deliberate in your choice to use `fx.Invoke` in your module. +By design, Fx executes constructors added via `fx.Provide` +only if the application consumes its result, either directly or indirectly, +through another module, constructor, or invoke. +On the other hand, functions added with `fx.Invoke` run unconditionally, +and in doing so instantiate every direct and transitive value they depend on.