diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c04a13253..71f42f8aa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,6 +23,10 @@ concurrency: group: "pages" cancel-in-progress: true + +env: + UV_VERSION: 0.3.3 + jobs: build: runs-on: ubuntu-latest @@ -41,16 +45,9 @@ jobs: with: ref: ${{ inputs.head }} - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: '16' - cache: 'yarn' - cache-dependency-path: docs/yarn.lock - - - name: Install dependencies - run: yarn install - working-directory: docs + - name: Install uv + run: | + curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" | sh - name: Build run: make docs @@ -58,7 +55,7 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: docs/dist/ + path: docs/_site deploy: needs: build # run only after a successful build @@ -69,7 +66,7 @@ jobs: environment: name: github-pages - url: ${{ steps.deployment.output.pages_url }} + url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index dac32c277..3f6266615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ --- -sidebarDepth: 0 -search: false +search: + exclude: true --- # Changelog diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7da4c5641..801caa9df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ --- -sidebarDepth: 2 -search: false +search: + exclude: true --- # Contributing @@ -12,12 +12,13 @@ please [open an issue](https://github.com/uber-go/fx/issues/new) describing your proposal. Discussing API changes ahead of time makes pull request review much smoother. -::: tip -You'll need to sign [Uber's CLA](https://cla-assistant.io/uber-go/fx) -before we can accept any of your contributions. -If necessary, a bot will remind -you to accept the CLA when you open your pull request. -::: +!!! tip + + You'll need to sign [Uber's CLA](https://cla-assistant.io/uber-go/fx) + before we can accept any of your contributions. + If necessary, a bot will remind + you to accept the CLA when you open your pull request. + ## Contribute code @@ -25,76 +26,72 @@ Set up your local development environment to contribute to Fx. 1. [Fork](https://github.com/uber-go/fx/fork), then clone the repository. - - - ```bash - git clone https://github.com/your_github_username/fx.git - cd fx - git remote add upstream https://github.com/uber-go/fx.git - git fetch upstream - ``` - - - - ```bash - gh repo fork --clone uber-go/fx - ``` - - + === "Git" + + ```bash + git clone https://github.com/your_github_username/fx.git + cd fx + git remote add upstream https://github.com/uber-go/fx.git + git fetch upstream + ``` + + === "GitHub CLI" + + ```bash + gh repo fork --clone uber-go/fx + ``` 2. Install Fx's dependencies: - ```bash - go mod download - ``` + ```bash + go mod download + ``` 3. Verify that tests and other checks pass locally. - ```bash - make lint - make test - ``` + ```bash + make lint + make test + ``` - Note that for `make lint` to work, - you must be using the latest stable version of Go. - If you're on an older version, you can still contribute your change, - but we may discover style violations when you open the pull request. + Note that for `make lint` to work, + you must be using the latest stable version of Go. + If you're on an older version, you can still contribute your change, + but we may discover style violations when you open the pull request. Next, make your changes. 1. Create a new feature branch. - ```bash - git checkout master - git pull - git checkout -b cool_new_feature - ``` + ```bash + git checkout master + git pull + git checkout -b cool_new_feature + ``` 2. Make your changes, and verify that all tests and lints still pass. - ```bash - $EDITOR app.go - make lint - make test - ``` + ```bash + $EDITOR app.go + make lint + make test + ``` 3. When you're satisfied with the change, push it to your fork and make a pull request. - - - ```bash - git push origin cool_new_feature - # Open a PR at https://github.com/uber-go/fx/compare - ``` - - - - ```bash - gh pr create - ``` - - + === "Git" + + ```bash + git push origin cool_new_feature + # Open a PR at https://github.com/uber-go/fx/compare + ``` + + === "GitHub CLI" + + ```bash + gh pr create + ``` At this point, you're waiting on us to review your changes. We *try* to respond to issues and pull requests within a few business days, @@ -115,18 +112,14 @@ To contribute documentation to Fx, 1. Set up your local development environment as you would to [contribute code](#contribute-code). -2. Install the documentation website dependencies. - - ```bash - cd docs - yarn install - ``` +2. [Install uv](https://docs.astral.sh/uv/getting-started/installation/). + We use this to manage our Python dependencies. 3. Run the development server. - ```bash - yarn dev - ``` + ```bash + make serve + ``` 4. Make your changes. @@ -208,84 +201,54 @@ Markdown will reflow this into a "normal" pargraph when rendering. All code samples in documentation must be buildable and testable. -To aid in this, we have two tools: +To make this possible, we put code samples in the "ex/" directory, +and use the [PyMdown Snippets extension](https://facelessuser.github.io/pymdown-extensions/extensions/snippets/) +to include them in the documentation. -- [mdox](https://github.com/bwplotka/mdox/) -- the `region` shell script +To include code snippets in your documentation, +take the following steps: -#### mdox +1. Add source code under the `ex/` directory. + Usually, the file will be placed in a directory + with a name matching the documentation file + that will include the snippet. -mdox is a Markdown file formatter that includes support for -running a command and using its output as part of a code block. -To use this, declare a regular code block and tag it with `mdoc-exec`. + For example, + for src/annotation.md, examples will reside in ex/annotation/. -```markdown -```go mdox-exec='cat foo.go' -// ... -``` +2. Inside the source file, name regions of code with comments in the forms: -The contents of the code block will be replaced -with the output of the command when you run `make fmt` -in the docs directory. -`make check` will ensure that the contents are up-to-date. + ``` + // --8<-- [start:name] + ... + // --8<-- [end:name] + ``` -The command runs with the working directory set to docs/. -Store code samples in ex/ and reference them directly. + Where `name` is the name of the snippet. + For example: -#### region + ```go + // --8<-- [start:New] + func New() *http.Server { + // ... + } + // --8<-- [end:New] + ``` -The `region` shell script is a command intended to be used with `mdox-exec`. +3. Include the snippet in a code block with the following syntax: -```plain mdox-exec='region' mdox-expect-exit-code='1' -USAGE: region FILE REGION1 REGION2 ... + ```markdown + ```go + ;--8<-- "path/to/file.go:name" + ``` -Extracts text from FILE marked by "// region" blocks. -``` + Where `path/to/file.go` is the path to the file containing the snippet + relative to the `ex/` directory, + and `name` is the name of the snippet. -For example, given the file: + You can include multiple snippets from the same file like so: -``` -foo -// region myregion -bar -// endregion myregion -baz -``` - -Running `region $FILE myregion` will print: - -``` -bar -``` - -The same region name may be used multiple times -to pull different snippets from the same file. -For example, given the file: - -```go -// region provide-foo -func main() { - fx.New( - fx.Provide( - NewFoo, - // endregion provide-foo - NewBar, - // region provide-foo - ), - ).Run() -} - -// endregion provide-foo -``` - -`region $FILE provide-foo` will print, - -```go -func main() { - fx.New( - fx.Provide( - NewFoo, - ), - ).Run() -} -``` + ``` + ;--8<-- "path/to/file.go:snippet1" + ;--8<-- "path/to/file.go:snippet2" + ``` diff --git a/Makefile b/Makefile index bbda9cc12..528300b22 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,6 @@ export GOBIN ?= $(PROJECT_ROOT)/bin export PATH := $(GOBIN):$(PATH) FXLINT = $(GOBIN)/fxlint -MDOX = $(GOBIN)/mdox MODULES = . ./tools ./docs ./internal/e2e @@ -21,7 +20,7 @@ build: go build ./... .PHONY: lint -lint: golangci-lint tidy-lint fx-lint docs-lint +lint: golangci-lint tidy-lint fx-lint .PHONY: test test: @@ -41,7 +40,7 @@ tidy: .PHONY: docs docs: - cd docs && yarn build + cd docs && make build .PHONY: golangci-lint golangci-lint: @@ -62,13 +61,5 @@ tidy-lint: fx-lint: $(FXLINT) @$(FXLINT) ./... -.PHONY: docs-lint -docs-lint: $(MDOX) - @echo "Checking documentation" - @make -C docs check - -$(MDOX): tools/go.mod - cd tools && go install github.com/bwplotka/mdox - $(FXLINT): tools/cmd/fxlint/main.go cd tools && go install go.uber.org/fx/tools/cmd/fxlint diff --git a/docs/.gitattributes b/docs/.gitattributes new file mode 100644 index 000000000..bbc83a592 --- /dev/null +++ b/docs/.gitattributes @@ -0,0 +1,2 @@ +# Mark the uv.lock as generated so it is collapsed in review by default. +uv.lock linguist-generated diff --git a/docs/.gitignore b/docs/.gitignore old mode 100755 new mode 100644 index 595e21518..0baf01522 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,12 +1 @@ -pids -logs -node_modules -npm-debug.log -coverage/ -run -dist -.DS_Store -.nyc_output -.basement -config.local.js -basement_dist +/_site diff --git a/docs/.mdox-validate.yaml b/docs/.mdox-validate.yaml deleted file mode 100644 index 5d8c180a5..000000000 --- a/docs/.mdox-validate.yaml +++ /dev/null @@ -1,10 +0,0 @@ -version: 1 -timeout: 1m - -validators: - # Instead of hitting every GitHub PR/issue links manually, - # use the GitHub API. - - regex: '(^http[s]?:\/\/)(www\.)?(github\.com\/)uber-go\/fx(\/pull\/|\/issues\/)' - type: 'githubPullsIssues' - - regex: '^http[s]://github.com/uber-go/fx/compare/[^/]*$' - type: 'ignore' diff --git a/docs/.npmrc b/docs/.npmrc deleted file mode 100644 index b18520343..000000000 --- a/docs/.npmrc +++ /dev/null @@ -1 +0,0 @@ -registry = https://registry.npmjs.org diff --git a/docs/.vuepress/components/Redirect.vue b/docs/.vuepress/components/Redirect.vue deleted file mode 100644 index f0d11609b..000000000 --- a/docs/.vuepress/components/Redirect.vue +++ /dev/null @@ -1,19 +0,0 @@ -// Credit https://github.com/vuejs/vuepress/issues/239#issuecomment-632567115 - - - - diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js deleted file mode 100755 index 0c637ec7c..000000000 --- a/docs/.vuepress/config.js +++ /dev/null @@ -1,133 +0,0 @@ -const { description } = require('../package') - -module.exports = { - // We're deploying to https://uber-go.github.io/fx/ - // so base should be /fx/. - base: '/fx/', - /** - * Ref:https://v1.vuepress.vuejs.org/config/#title - */ - title: 'Fx', - /** - * Ref:https://v1.vuepress.vuejs.org/config/#description - */ - description: description, - - dest: 'dist', // Publish built website to dist. We'll feed this to GitHub. - - /** - * Extra tags to be injected to the page HTML `` - * - * ref:https://v1.vuepress.vuejs.org/config/#head - */ - head: [ - ['meta', { name: 'theme-color', content: '#3eaf7c' }], - ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], - ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], - ['script', - { - async: true, - src: 'https://www.googletagmanager.com/gtag/js?id=G-4YWLTPJ46M' - } - ], - ['script', {}, - [ - "window.dataLayer = window.dataLayer || [];\n"+ - "function gtag(){dataLayer.push(arguments);}\n"+ - "gtag('js', new Date());\n"+ - "gtag('config', 'G-4YWLTPJ46M');" - ] - ] - ], - - /** - * Theme configuration, here is the default theme configuration for VuePress. - * - * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html - */ - themeConfig: { - repo: 'uber-go/fx', - editLinks: true, - docsDir: 'docs', - lastUpdated: true, - nav: [ - { - text: 'Guide', - link: '/intro', - }, - { - text: 'API Reference', - link: 'https://pkg.go.dev/go.uber.org/fx' - } - ], - sidebar: [ - { - title: 'Get Started', - path: '/get-started/', - children: [ - 'get-started/minimal.md', - 'get-started/http-server.md', - 'get-started/echo-handler.md', - 'get-started/logger.md', - 'get-started/registration.md', - 'get-started/another-handler.md', - 'get-started/many-handlers.md', - 'get-started/conclusion.md', - ], - }, - 'intro.md', - { - title: 'Concepts', - children: [ - 'container.md', - ['lifecycle.md', 'Lifecycle'], - 'modules.md', - ], - }, - { - title: 'Features', - children: [ - 'parameter-objects.md', - 'result-objects.md', - 'annotate.md', - { - title: 'Value Groups', - path: '/value-groups/', - children: [ - { - title: 'Feeding', - path: 'value-groups/feed.md', - }, - { - title: 'Consuming', - path: 'value-groups/consume.md', - }, - ], - }, - ], - }, - ['faq.md', 'FAQ'], - { - title: 'Community', - children: [ - 'contributing.md', - ], - }, - { - title: 'Release notes', - path: 'changelog.md', - }, - ] - }, - - /** - * Apply plugins,ref:https://v1.vuepress.vuejs.org/zh/plugin/ - */ - plugins: [ - '@vuepress/plugin-back-to-top', - '@vuepress/plugin-medium-zoom', - 'fulltext-search', - 'vuepress-plugin-mermaidjs', - 'vuepress-plugin-code-copy', - ] -} diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js deleted file mode 100755 index 8452a8689..000000000 --- a/docs/.vuepress/enhanceApp.js +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Client app enhancement file. - * - * https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements - */ - -export default ({ - Vue, // the version of Vue being used in the VuePress app - options, // the options for the root Vue instance - router, // the router instance for the app - siteData // site metadata -}) => { - // ...apply enhancements for the site. -} diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl deleted file mode 100755 index 420feb93f..000000000 --- a/docs/.vuepress/styles/index.styl +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Custom Styles here. - * - * ref:https://v1.vuepress.vuejs.org/config/#index-styl - */ - -.home .hero img - max-width 450px!important diff --git a/docs/.vuepress/styles/palette.styl b/docs/.vuepress/styles/palette.styl deleted file mode 100755 index 6490cb359..000000000 --- a/docs/.vuepress/styles/palette.styl +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Custom palette here. - * - * ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl - */ - -$accentColor = #3eaf7c -$textColor = #2c3e50 -$borderColor = #eaecef -$codeBgColor = #282c34 diff --git a/docs/.yarnrc b/docs/.yarnrc deleted file mode 100644 index b4a8ce4f9..000000000 --- a/docs/.yarnrc +++ /dev/null @@ -1 +0,0 @@ -registry "https://registry.yarnpkg.com" diff --git a/docs/Makefile b/docs/Makefile index d641d2347..cfb5790ab 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,16 +1,7 @@ -export PATH := $(shell pwd)/bin:$(PATH) +.PHONY: build +build: + uv run mkdocs build -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') - -.PHONY: -fmt: $(MDOX) - $(MDOX) fmt $(MDOX_FMT_FLAGS) $(MD_FILES) - -.PHONY: -check: $(MDOX) - $(MDOX) fmt --check $(MDOX_FMT_FLAGS) $(MD_FILES) - -$(MDOX): - make -C .. $(shell pwd)/bin/mdox +.PHONY: serve +serve: + uv run mkdocs serve diff --git a/docs/bin/region b/docs/bin/region deleted file mode 100755 index 0b05de3c4..000000000 --- a/docs/bin/region +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -IFS=$'\n\t' - -if [[ $# -lt 2 ]]; then - echo >&2 'USAGE: region FILE REGION1 REGION2 ...' - echo >&2 - echo >&2 'Extracts text from FILE marked by "// region" blocks.' - exit 1 -fi - -file="$1"; shift - -args=(-n) -for region in "$@"; do - # sed syntax: - # We can either use /regex/, or \CregexC for any C. - # Since we need to match on "//", we use "#" as the regex delimiter. - # - # And we can use $expr1,$expr2p to say "print lines inside that - # region." - open='\#// region '"$region"'$#' - close='\#// endregion '"$region"'$#' - args+=(-e "${open},${close}p") -done - -sed -n "${args[@]}" "$file" | - grep -Ev '// (end)?region \S+$' | - perl -ne ' - # Remove a leading/trailing empty line, if any. - if ((!$saw_first || eof) && /^\s*$/) { - next; - } - $saw_first = true; - - s/\t/ /g; - print; - ' diff --git a/docs/changelog.md b/docs/changelog.md deleted file mode 120000 index 04c99a55c..000000000 --- a/docs/changelog.md +++ /dev/null @@ -1 +0,0 @@ -../CHANGELOG.md \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md deleted file mode 120000 index 44fcc6343..000000000 --- a/docs/contributing.md +++ /dev/null @@ -1 +0,0 @@ -../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/ex/annotate/cast.go b/docs/ex/annotate/cast.go index 89496308d..af951e5a8 100644 --- a/docs/ex/annotate/cast.go +++ b/docs/ex/annotate/cast.go @@ -28,7 +28,7 @@ import ( ) // HTTPClient matches the http.Client interface. -// region interface +// --8<-- [start:interface] type HTTPClient interface { Do(*http.Request) (*http.Response, error) } @@ -37,28 +37,28 @@ type HTTPClient interface { // that our interface matches the API of http.Client. var _ HTTPClient = (*http.Client)(nil) -// endregion interface +// --8<-- [end:interface] // Config specifies the configuration of a client. type Config struct{} // NewHTTPClient builds a new HTTP client. -// region constructor +// --8<-- [start:constructor] func NewHTTPClient(Config) (*http.Client, error) { - // endregion constructor + // --8<-- [end:constructor] return http.DefaultClient, nil } // NewGitHubClient builds a new GitHub client. -// region iface-consumer +// --8<-- [start:iface-consumer] func NewGitHubClient(client HTTPClient) *github.Client { - // endregion iface-consumer + // --8<-- [end:iface-consumer] return new(github.Client) } func options() fx.Option { return fx.Options( - // region provides + // --8<-- [start:provides] fx.Provide( fx.Annotate( NewHTTPClient, @@ -66,6 +66,6 @@ func options() fx.Option { ), NewGitHubClient, ), - // endregion provides + // --8<-- [end:provides] ) } diff --git a/docs/ex/annotate/cast_bad.go b/docs/ex/annotate/cast_bad.go index bbe706187..c2ae3a882 100644 --- a/docs/ex/annotate/cast_bad.go +++ b/docs/ex/annotate/cast_bad.go @@ -31,19 +31,19 @@ import ( ) // NewGitHubClient builds a new GitHub client. -// region struct-consumer +// --8<-- [start:struct-consumer] func NewGitHubClient(client *http.Client) *github.Client { - // endregion struct-consumer + // --8<-- [end:struct-consumer] return new(github.Client) } func options() fx.Option { return fx.Options( - // region provides + // --8<-- [start:provides] fx.Provide( NewHTTPClient, NewGitHubClient, ), - // endregion provides + // --8<-- [end:provides] ) } diff --git a/docs/ex/annotate/sample.go b/docs/ex/annotate/sample.go index 24b1df13f..aa5b483a6 100644 --- a/docs/ex/annotate/sample.go +++ b/docs/ex/annotate/sample.go @@ -26,25 +26,25 @@ import ( func howToAnnotate() (before, after fx.Option) { before = fx.Options( - // region before + // --8<-- [start:before] fx.Provide( NewHTTPClient, ), - // endregion before + // --8<-- [end:before] ) after = fx.Options( - // region wrap - // region annotate + // --8<-- [start:wrap-1] + // --8<-- [start:annotate] fx.Provide( fx.Annotate( NewHTTPClient, - // endregion wrap + // --8<-- [end:wrap-1] fx.ResultTags(`name:"client"`), - // region wrap + // --8<-- [start:wrap-2] ), ), - // endregion annotate - // endregion wrap + // --8<-- [end:annotate] + // --8<-- [end:wrap-2] ) return before, after } diff --git a/docs/ex/get-started/01-minimal/main.go b/docs/ex/get-started/01-minimal/main.go index 614142de5..28288b75e 100644 --- a/docs/ex/get-started/01-minimal/main.go +++ b/docs/ex/get-started/01-minimal/main.go @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -// region main +// --8<-- [start:main] package main @@ -28,4 +28,4 @@ func main() { fx.New().Run() } -// endregion main +// --8<-- [end:main] diff --git a/docs/ex/get-started/02-http-server/main.go b/docs/ex/get-started/02-http-server/main.go index 0b6feef42..708fbe845 100644 --- a/docs/ex/get-started/02-http-server/main.go +++ b/docs/ex/get-started/02-http-server/main.go @@ -29,28 +29,28 @@ import ( "go.uber.org/fx" ) -// region provide-server +// --8<-- [start:provide-server-1] func main() { - // region app + // --8<-- [start:app] fx.New( fx.Provide(NewHTTPServer), - // endregion provide-server + // --8<-- [end:provide-server-1] fx.Invoke(func(*http.Server) {}), - // region provide-server + // --8<-- [start:provide-server-2] ).Run() - // endregion app + // --8<-- [end:app] } -// endregion provide-server +// --8<-- [end:provide-server-2] -// region partial +// --8<-- [start:partial-1] // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. -// region full +// --8<-- [start:full] func NewHTTPServer(lc fx.Lifecycle) *http.Server { srv := &http.Server{Addr: ":8080"} - // endregion partial + // --8<-- [end:partial-1] lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) @@ -65,9 +65,9 @@ func NewHTTPServer(lc fx.Lifecycle) *http.Server { return srv.Shutdown(ctx) }, }) - // region partial + // --8<-- [start:partial-2] return srv } -// endregion partial -// region full +// --8<-- [end:partial-2] +// --8<-- [end:full] diff --git a/docs/ex/get-started/03-echo-handler/main.go b/docs/ex/get-started/03-echo-handler/main.go index 0384bdc94..af3c1863d 100644 --- a/docs/ex/get-started/03-echo-handler/main.go +++ b/docs/ex/get-started/03-echo-handler/main.go @@ -33,22 +33,22 @@ import ( func main() { fx.New( - // region provides - // region provide-handler + // --8<-- [start:provides] + // --8<-- [start:provide-handler-1] fx.Provide( NewHTTPServer, - // endregion provide-handler + // --8<-- [end:provide-handler-1] NewServeMux, - // region provide-handler + // --8<-- [start:provide-handler-2] NewEchoHandler, ), - // endregion provides + // --8<-- [end:provides] fx.Invoke(func(*http.Server) {}), - // endregion provide-handler + // --8<-- [end:provide-handler-2] ).Run() } -// region serve-mux +// --8<-- [start:serve-mux] // NewServeMux builds a ServeMux that will route requests // to the given EchoHandler. @@ -58,9 +58,9 @@ func NewServeMux(echo *EchoHandler) *http.ServeMux { return mux } -// endregion serve-mux +// --8<-- [end:serve-mux] -// region echo-handler +// --8<-- [start:echo-handler] // EchoHandler is an http.Handler that copies its request body // back to the response. @@ -78,15 +78,15 @@ func (*EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -// endregion echo-handler +// --8<-- [end:echo-handler] // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. -// region connect-mux +// --8<-- [start:connect-mux] func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux) *http.Server { srv := &http.Server{Addr: ":8080", Handler: mux} lc.Append(fx.Hook{ - // endregion connect-mux + // --8<-- [end:connect-mux] OnStart: func(ctx context.Context) error { ln, err := net.Listen("tcp", srv.Addr) if err != nil { diff --git a/docs/ex/get-started/04-logger/main.go b/docs/ex/get-started/04-logger/main.go index e229b6f43..f6db423cc 100644 --- a/docs/ex/get-started/04-logger/main.go +++ b/docs/ex/get-started/04-logger/main.go @@ -31,21 +31,21 @@ import ( "go.uber.org/zap" ) -// region fx-logger +// --8<-- [start:fx-logger] func main() { fx.New( fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), - // endregion fx-logger - // region provides + // --8<-- [end:fx-logger] + // --8<-- [start:provides] fx.Provide( NewHTTPServer, NewServeMux, NewEchoHandler, zap.NewExample, ), - // endregion provides + // --8<-- [end:provides] fx.Invoke(func(*http.Server) {}), ).Run() } @@ -60,34 +60,34 @@ func NewServeMux(echo *EchoHandler) *http.ServeMux { // EchoHandler is an http.Handler that copies its request body // back to the response. -// region echo-init +// --8<-- [start:echo-init-1] type EchoHandler struct { log *zap.Logger } -// endregion echo-init +// --8<-- [end:echo-init-1] // NewEchoHandler builds a new EchoHandler. -// region echo-init +// --8<-- [start:echo-init-2] func NewEchoHandler(log *zap.Logger) *EchoHandler { return &EchoHandler{log: log} } -// endregion echo-init +// --8<-- [end:echo-init-2] // ServeHTTP handles an HTTP request to the /echo endpoint. -// region echo-serve +// --8<-- [start:echo-serve] func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if _, err := io.Copy(w, r.Body); err != nil { h.log.Warn("Failed to handle request", zap.Error(err)) } } -// endregion echo-serve +// --8<-- [end:echo-serve] // NewHTTPServer builds an HTTP server that will begin serving requests // when the Fx application starts. -// region http-server +// --8<-- [start:http-server] func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server { srv := &http.Server{Addr: ":8080", Handler: mux} lc.Append(fx.Hook{ @@ -98,7 +98,7 @@ func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.S } log.Info("Starting HTTP server", zap.String("addr", srv.Addr)) go srv.Serve(ln) - // endregion http-server + // --8<-- [end:http-server] return nil }, OnStop: func(ctx context.Context) error { diff --git a/docs/ex/get-started/05-registration/main.go b/docs/ex/get-started/05-registration/main.go index ead1deab0..ac2b23d25 100644 --- a/docs/ex/get-started/05-registration/main.go +++ b/docs/ex/get-started/05-registration/main.go @@ -36,7 +36,7 @@ func main() { fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), - // region provides + // --8<-- [start:provides] fx.Provide( NewHTTPServer, NewServeMux, @@ -46,12 +46,12 @@ func main() { ), zap.NewExample, ), - // endregion provides + // --8<-- [end:provides] fx.Invoke(func(*http.Server) {}), ).Run() } -// region route +// --8<-- [start:route] // Route is an http.Handler that knows the mux pattern // under which it will be registered. @@ -62,9 +62,9 @@ type Route interface { Pattern() string } -// endregion route +// --8<-- [end:route] -// region mux +// --8<-- [start:mux] // NewServeMux builds a ServeMux that will route requests // to the given Route. @@ -74,7 +74,7 @@ func NewServeMux(route Route) *http.ServeMux { return mux } -// endregion mux +// --8<-- [end:mux] // EchoHandler is an http.Handler that copies its request body // back to the response. @@ -89,12 +89,12 @@ func NewEchoHandler(log *zap.Logger) *EchoHandler { // Pattern reports the pattern under which // this handler should be registered. -// region echo-pattern +// --8<-- [start:echo-pattern] func (*EchoHandler) Pattern() string { return "/echo" } -// endregion echo-pattern +// --8<-- [end:echo-pattern] // ServeHTTP handles an HTTP request to the /echo endpoint. func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/docs/ex/get-started/06-another-handler/main.go b/docs/ex/get-started/06-another-handler/main.go index a4c051add..f30593732 100644 --- a/docs/ex/get-started/06-another-handler/main.go +++ b/docs/ex/get-started/06-another-handler/main.go @@ -37,32 +37,32 @@ func main() { fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), - // region mux-provide + // --8<-- [start:mux-provide] fx.Provide( NewHTTPServer, fx.Annotate( NewServeMux, fx.ParamTags(`name:"echo"`, `name:"hello"`), ), - // endregion mux-provide - // region hello-provide-partial - // region route-provides + // --8<-- [end:mux-provide] + // --8<-- [start:hello-provide-partial-1] + // --8<-- [start:route-provides] fx.Annotate( NewEchoHandler, fx.As(new(Route)), - // endregion hello-provide-partial + // --8<-- [end:hello-provide-partial-1] fx.ResultTags(`name:"echo"`), - // region hello-provide-partial + // --8<-- [start:hello-provide-partial-2] ), fx.Annotate( NewHelloHandler, fx.As(new(Route)), - // endregion hello-provide-partial + // --8<-- [end:hello-provide-partial-2] fx.ResultTags(`name:"hello"`), - // region hello-provide-partial + // --8<-- [start:hello-provide-partial-3] ), - // endregion hello-provide-partial - // endregion route-provides + // --8<-- [end:hello-provide-partial-3] + // --8<-- [end:route-provides] zap.NewExample, ), fx.Invoke(func(*http.Server) {}), @@ -78,7 +78,7 @@ type Route interface { Pattern() string } -// region mux +// --8<-- [start:mux] // NewServeMux builds a ServeMux that will route requests // to the given routes. @@ -89,9 +89,9 @@ func NewServeMux(route1, route2 Route) *http.ServeMux { return mux } -// endregion mux +// --8<-- [end:mux] -// region hello-init +// --8<-- [start:hello-init] // HelloHandler is an HTTP handler that // prints a greeting to the user. @@ -104,19 +104,19 @@ func NewHelloHandler(log *zap.Logger) *HelloHandler { return &HelloHandler{log: log} } -// endregion hello-init +// --8<-- [end:hello-init] // Pattern reports the pattern under which // this handler should be registered. -// region hello-methods +// --8<-- [start:hello-methods-1] func (*HelloHandler) Pattern() string { return "/hello" } -// endregion hello-methods +// --8<-- [end:hello-methods-1] // ServeHTTP handles an HTTP request to the /hello endpoint. -// region hello-methods +// --8<-- [start:hello-methods-2] func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { @@ -132,7 +132,7 @@ func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -// endregion hello-methods +// --8<-- [end:hello-methods-2] // EchoHandler is an http.Handler that copies its request body // back to the response. diff --git a/docs/ex/get-started/07-many-handlers/main.go b/docs/ex/get-started/07-many-handlers/main.go index 2dcccae1f..aae7296d7 100644 --- a/docs/ex/get-started/07-many-handlers/main.go +++ b/docs/ex/get-started/07-many-handlers/main.go @@ -37,27 +37,27 @@ func main() { fx.WithLogger(func(log *zap.Logger) fxevent.Logger { return &fxevent.ZapLogger{Logger: log} }), - // region mux-provide - // region route-provides + // --8<-- [start:mux-provide] + // --8<-- [start:route-provides-1] fx.Provide( - // endregion route-provides + // --8<-- [end:route-provides-1] NewHTTPServer, fx.Annotate( NewServeMux, fx.ParamTags(`group:"routes"`), ), - // endregion mux-provide - // region route-provides + // --8<-- [end:mux-provide] + // --8<-- [start:route-provides-2] AsRoute(NewEchoHandler), AsRoute(NewHelloHandler), zap.NewExample, ), - // endregion route-provides + // --8<-- [end:route-provides-2] fx.Invoke(func(*http.Server) {}), ).Run() } -// region AsRoute +// --8<-- [start:AsRoute] // AsRoute annotates the given constructor to state that // it provides a route to the "routes" group. @@ -69,7 +69,7 @@ func AsRoute(f any) any { ) } -// endregion AsRoute +// --8<-- [end:AsRoute] // Route is an http.Handler that knows the mux pattern // under which it will be registered. @@ -82,7 +82,7 @@ type Route interface { // NewServeMux builds a ServeMux that will route requests // to the given routes. -// region mux +// --8<-- [start:mux] func NewServeMux(routes []Route) *http.ServeMux { mux := http.NewServeMux() for _, route := range routes { @@ -91,7 +91,7 @@ func NewServeMux(routes []Route) *http.ServeMux { return mux } -// endregion mux +// --8<-- [end:mux] // HelloHandler is an HTTP handler that // prints a greeting to the user. diff --git a/docs/ex/modules/module.go b/docs/ex/modules/module.go index 538067580..ed197086a 100644 --- a/docs/ex/modules/module.go +++ b/docs/ex/modules/module.go @@ -26,54 +26,45 @@ import ( ) // Module is an example of an Fx module's skeleton. -// region start -// region provide -// region invoke -// region decorate -// region private +// --8<-- [start:start] var Module = fx.Module("server", - // endregion start + // --8<-- [end:start] + // --8<-- [start:provide] fx.Provide( New, - // endregion provide - // endregion invoke - // endregion decorate ), + // --8<-- [end:provide] + // --8<-- [start:privateProvide] fx.Provide( fx.Private, - // region provide - // region invoke - // region decorate parseConfig, ), - // endregion provide + // --8<-- [end:privateProvide] + // --8<-- [start:invoke] fx.Invoke(startServer), - // endregion invoke + // --8<-- [end:invoke] + // --8<-- [start:decorate] fx.Decorate(wrapLogger), - -// region provide -// region invoke + // --8<-- [end:decorate] +// --8<-- [start:endProvide] ) -// endregion invoke -// endregion provide -// endregion decorate -// endregion private +// --8<-- [end:endProvide] // Config is the configuration of the server. -// region config +// --8<-- [start:config] type Config struct { Addr string `yaml:"addr"` } -// endregion config +// --8<-- [end:config] func parseConfig() (Config, error) { return Config{}, nil } // Params defines the parameters of the module. -// region params +// --8<-- [start:params] type Params struct { fx.In @@ -81,22 +72,22 @@ type Params struct { Config Config } -// endregion params +// --8<-- [end:params] // Result defines the results of the module. -// region result +// --8<-- [start:result] type Result struct { fx.Out Server *Server } -// endregion result +// --8<-- [end:result] // New builds a new server. -// region new +// --8<-- [start:new] func New(p Params) (Result, error) { - // endregion new + // --8<-- [end:new] return Result{ Server: &Server{}, }, nil diff --git a/docs/ex/parameter-objects/define.go b/docs/ex/parameter-objects/define.go index 3cacb2527..16b3adef7 100644 --- a/docs/ex/parameter-objects/define.go +++ b/docs/ex/parameter-objects/define.go @@ -40,31 +40,31 @@ type ClientConfig struct { } // ClientParams defines the parameters necessary to build a client. -// region empty -// region fxin -// region fields +// --8<-- [start:empty-1] +// --8<-- [start:fxin] +// --8<-- [start:fields] type ClientParams struct { - // endregion empty + // --8<-- [end:empty-1] fx.In - // endregion fxin + // --8<-- [end:fxin] Config ClientConfig HTTPClient *http.Client - // region empty + // --8<-- [start:empty-2] } -// endregion fields -// endregion empty +// --8<-- [end:fields] +// --8<-- [end:empty-2] // NewClient builds a new client. -// region takeparam -// region consume +// --8<-- [start:takeparam] +// --8<-- [start:consume] func NewClient(p ClientParams) (*Client, error) { - // endregion takeparam + // --8<-- [end:takeparam] return &Client{ url: p.Config.URL, http: p.HTTPClient, // ... }, nil - // endregion consume + // --8<-- [end:consume] } diff --git a/docs/ex/parameter-objects/extend.go b/docs/ex/parameter-objects/extend.go index a246d7460..cb925489c 100644 --- a/docs/ex/parameter-objects/extend.go +++ b/docs/ex/parameter-objects/extend.go @@ -28,32 +28,32 @@ import ( ) // Params defines the parameters of new. -// region start -// region full +// --8<-- [start:start-1] +// --8<-- [start:full] type Params struct { fx.In Config ClientConfig HTTPClient *http.Client - // endregion start + // --8<-- [end:start-1] Logger *zap.Logger `optional:"true"` - // region start + // --8<-- [start:start-2] } -// endregion start -// endregion full +// --8<-- [end:start-2] +// --8<-- [end:full] // New builds a new Client. -// region start -// region consume +// --8<-- [start:start-3] +// --8<-- [start:consume] func New(p Params) (*Client, error) { - // endregion start + // --8<-- [end:start-3] log := p.Logger if log == nil { log = zap.NewNop() } // ... - // endregion consume + // --8<-- [end:consume] return &Client{log: log}, nil } diff --git a/docs/ex/result-objects/define.go b/docs/ex/result-objects/define.go index 70d079692..400fbf36d 100644 --- a/docs/ex/result-objects/define.go +++ b/docs/ex/result-objects/define.go @@ -26,30 +26,30 @@ import "go.uber.org/fx" type Client struct{} // ClientResult holds the result of NewClient. -// region empty -// region fxout -// region fields +// --8<-- [start:empty-1] +// --8<-- [start:fxout] +// --8<-- [start:fields] type ClientResult struct { - // endregion empty + // --8<-- [end:empty-1] fx.Out - // endregion fxout + // --8<-- [end:fxout] Client *Client - // region empty + // --8<-- [start:empty-2] } -// endregion empty -// endregion fields +// --8<-- [end:empty-2] +// --8<-- [end:fields] // NewClient builds a new Client. -// region returnresult -// region produce +// --8<-- [start:returnresult] +// --8<-- [start:produce] func NewClient() (ClientResult, error) { - // endregion returnresult + // --8<-- [end:returnresult] client := &Client{ // ... } return ClientResult{Client: client}, nil } -// endregion produce +// --8<-- [end:produce] diff --git a/docs/ex/result-objects/extend.go b/docs/ex/result-objects/extend.go index e479563c3..37e5d8b45 100644 --- a/docs/ex/result-objects/extend.go +++ b/docs/ex/result-objects/extend.go @@ -26,35 +26,35 @@ import "go.uber.org/fx" type Inspector struct{} // Result is the result of this module. -// region full -// region start +// --8<-- [start:full] +// --8<-- [start:start-1] type Result struct { fx.Out Client *Client - // endregion start + // --8<-- [end:start-1] Inspector *Inspector - // region start + // --8<-- [start:start-2] } -// endregion start -// endregion full +// --8<-- [end:start-2] +// --8<-- [end:full] // New builds a result. -// region start +// --8<-- [start:start-3] func New() (Result, error) { client := &Client{ // ... } - // region produce + // --8<-- [start:produce] return Result{ Client: client, - // endregion start + // --8<-- [end:start-3] Inspector: &Inspector{ // ... }, - // region start + // --8<-- [start:start-4] }, nil - // endregion start - // endregion produce + // --8<-- [end:start-4] + // --8<-- [end:produce] } diff --git a/docs/ex/value-groups/consume/annotate.go b/docs/ex/value-groups/consume/annotate.go index 2ce58f934..9f1aad054 100644 --- a/docs/ex/value-groups/consume/annotate.go +++ b/docs/ex/value-groups/consume/annotate.go @@ -24,40 +24,40 @@ import "go.uber.org/fx" // PlainModule is an unannotated NewEmitter. var PlainModule = fx.Options( - // region provide-init + // --8<-- [start:provide-init] fx.Provide( NewEmitter, ), - // endregion provide-init + // --8<-- [end:provide-init] ) // AnnotateModule is the module defined in this file. var AnnotateModule = fx.Options( - // region provide-wrap + // --8<-- [start:provide-wrap-1] fx.Provide( - // region provide-annotate + // --8<-- [start:provide-annotate] fx.Annotate( NewEmitter, - // endregion provide-wrap + // --8<-- [end:provide-wrap-1] fx.ParamTags(`group:"watchers"`), - // region provide-wrap + // --8<-- [start:provide-wrap-2] ), - // endregion provide-annotate + // --8<-- [end:provide-annotate] ), - // endregion provide-wrap + // --8<-- [end:provide-wrap-2] ) // Emitter emits events type Emitter struct{ ws []Watcher } // NewEmitter builds an emitter. -// region new-init -// region new-consume +// --8<-- [start:new-init] +// --8<-- [start:new-consume] func NewEmitter(watchers []Watcher) (*Emitter, error) { - // endregion new-init + // --8<-- [end:new-init] for _, w := range watchers { // ... - // endregion new-consume + // --8<-- [end:new-consume] _ = w // unused } return &Emitter{ws: watchers}, nil @@ -66,18 +66,18 @@ func NewEmitter(watchers []Watcher) (*Emitter, error) { // EmitterFromModule is a module that holds EmitterFrom. var EmitterFromModule = fx.Options( fx.Provide( - // region annotate-variadic + // --8<-- [start:annotate-variadic] fx.Annotate( EmitterFrom, fx.ParamTags(`group:"watchers"`), ), - // endregion annotate-variadic + // --8<-- [end:annotate-variadic] ), ) // EmitterFrom builds an Emitter from the list of watchers. -// region new-variadic +// --8<-- [start:new-variadic] func EmitterFrom(watchers ...Watcher) (*Emitter, error) { - // region new-variadic + // --8<-- [end:new-variadic] return &Emitter{ws: watchers}, nil } diff --git a/docs/ex/value-groups/consume/param.go b/docs/ex/value-groups/consume/param.go index ce7dd5bc8..8fedec3f5 100644 --- a/docs/ex/value-groups/consume/param.go +++ b/docs/ex/value-groups/consume/param.go @@ -27,25 +27,25 @@ type Watcher interface{} // ParamsModule is the module defined in this file. var ParamsModule = fx.Options( - // region provide + // --8<-- [start:provide] fx.Provide(New), - // endregion provide + // --8<-- [end:provide] ) // Params is a parameter object. -// region param-tagged -// region param-init +// --8<-- [start:param-tagged] +// --8<-- [start:param-init-1] type Params struct { fx.In // ... - // endregion param-init + // --8<-- [end:param-init-1] Watchers []Watcher `group:"watchers"` - // region param-init + // --8<-- [start:param-init-2] } -// endregion param-init -// endregion param-tagged +// --8<-- [end:param-init-2] +// --8<-- [end:param-tagged] // Result is a list of watchers. type Result struct { @@ -55,14 +55,14 @@ type Result struct { } // New consumes a value group. -// region new-init -// region new-consume +// --8<-- [start:new-init] +// --8<-- [start:new-consume] func New(p Params) (Result, error) { // ... - // endregion new-init + // --8<-- [end:new-init] for _, w := range p.Watchers { // ... - // endregion new-consume + // --8<-- [end:new-consume] _ = w // unused } return Result{ diff --git a/docs/ex/value-groups/feed/annotate.go b/docs/ex/value-groups/feed/annotate.go index fec7870b7..bae1ad2e6 100644 --- a/docs/ex/value-groups/feed/annotate.go +++ b/docs/ex/value-groups/feed/annotate.go @@ -24,23 +24,23 @@ import "go.uber.org/fx" // AnnotateModule is the module defined in this file. var AnnotateModule = fx.Options( - // region provide-init + // --8<-- [start:provide-init] fx.Provide( NewWatcher, ), - // endregion provide-init - // region provide-wrap + // --8<-- [end:provide-init] + // --8<-- [start:provide-wrap-1] fx.Provide( - // region provide-annotate + // --8<-- [start:provide-annotate] fx.Annotate( NewWatcher, - // endregion provide-wrap + // --8<-- [end:provide-wrap-1] fx.ResultTags(`group:"watchers"`), - // region provide-wrap + // --8<-- [start:provide-wrap-2] ), - // endregion provide-annotate + // --8<-- [end:provide-annotate] ), - // endregion provide-wrap + // --8<-- [end:provide-wrap-2] ) // FileWatcher watches files. @@ -49,30 +49,30 @@ type FileWatcher struct{} // FileWatcherModule provides a FileWatcher as a Watcher. var FileWatcherModule = fx.Options( fx.Provide( - // region annotate-fw + // --8<-- [start:annotate-fw] fx.Annotate( NewFileWatcher, fx.As(new(Watcher)), fx.ResultTags(`group:"watchers"`), ), - // endregion annotate-fw + // --8<-- [end:annotate-fw] ), ) // NewFileWatcher builds a new file watcher. -// region new-fw-init +// --8<-- [start:new-fw-init] func NewFileWatcher( /* ... */ ) (*FileWatcher, error) { - // endregion new-fw-init + // --8<-- [end:new-fw-init] return &FileWatcher{ // ... }, nil } // NewWatcher builds a watcher. -// region new-init +// --8<-- [start:new-init] func NewWatcher( /* ... */ ) (Watcher, error) { // ... - // endregion new-init + // --8<-- [end:new-init] return &FileWatcher{ // ... diff --git a/docs/ex/value-groups/feed/result.go b/docs/ex/value-groups/feed/result.go index 7dd4278bd..60621d235 100644 --- a/docs/ex/value-groups/feed/result.go +++ b/docs/ex/value-groups/feed/result.go @@ -24,9 +24,9 @@ import "go.uber.org/fx" // ResultModule is the module defined in this file. var ResultModule = fx.Options( - // region provide + // --8<-- [start:provide] fx.Provide(New), - // endregion provide + // --8<-- [end:provide] ) // Watcher watches for events. @@ -35,36 +35,36 @@ type Watcher interface{} type watcher struct{} // Result is the result of an operation. -// region result-tagged -// region result-init +// --8<-- [start:result-tagged] +// --8<-- [start:result-init-1] type Result struct { fx.Out // ... - // endregion result-init + // --8<-- [end:result-init-1] Watcher Watcher `group:"watchers"` - // region result-init + // --8<-- [start:result-init-2] } -// endregion result-init -// endregion result-tagged +// --8<-- [end:result-init-2] +// --8<-- [end:result-tagged] // New produces a result object. -// region new-init -// region new-watcher +// --8<-- [start:new-init-1] +// --8<-- [start:new-watcher] func New( /* ... */ ) (Result, error) { // ... - // endregion new-init + // --8<-- [end:new-init-1] watcher := &watcher{ // ... } - // region new-init + // --8<-- [start:new-init-2] return Result{ // ... Watcher: watcher, }, nil } -// endregion new-watcher -// endregion new-init +// --8<-- [end:new-watcher] +// --8<-- [end:new-init-2] diff --git a/docs/get-started/another-handler.md b/docs/get-started/another-handler.md deleted file mode 100644 index b2ece1287..000000000 --- a/docs/get-started/another-handler.md +++ /dev/null @@ -1,158 +0,0 @@ -# Register another handler - -The handler we defined above has a single handler. -Let's add another. - -1. Build a new handler in the same file. - - ```go mdox-exec='region ex/get-started/06-another-handler/main.go hello-init' - // HelloHandler is an HTTP handler that - // prints a greeting to the user. - type HelloHandler struct { - log *zap.Logger - } - - // NewHelloHandler builds a new HelloHandler. - func NewHelloHandler(log *zap.Logger) *HelloHandler { - return &HelloHandler{log: log} - } - ``` - -2. Implement the `Route` interface for this handler. - - ```go mdox-exec='region ex/get-started/06-another-handler/main.go hello-methods' - func (*HelloHandler) Pattern() string { - return "/hello" - } - - func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - if err != nil { - h.log.Error("Failed to read request", zap.Error(err)) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - - if _, err := fmt.Fprintf(w, "Hello, %s\n", body); err != nil { - h.log.Error("Failed to write response", zap.Error(err)) - http.Error(w, "Internal server error", http.StatusInternalServerError) - return - } - } - ``` - - The handler reads its request body, - and writes a welcome message back to the caller. - -3. Provide this to the application as a `Route` next to `NewEchoHandler`. - - ```go mdox-exec='region ex/get-started/06-another-handler/main.go hello-provide-partial' - fx.Annotate( - NewEchoHandler, - fx.As(new(Route)), - ), - fx.Annotate( - NewHelloHandler, - fx.As(new(Route)), - ), - ``` - -4. Run the application--the service will fail to start. - - ``` - [Fx] PROVIDE *http.Server <= main.NewHTTPServer() - [Fx] PROVIDE *http.ServeMux <= main.NewServeMux() - [Fx] PROVIDE main.Route <= fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]]) - [Fx] Error after options were applied: fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from: - [...] - [Fx] ERROR Failed to start: the following errors occurred: - - fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from: - [...] - Failed: cannot provide function "main".NewHelloHandler ([..]/main.go:53): cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80) - ``` - - That's a lot of output, but inside the error message, we see: - - ``` - cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80) - ``` - - This fails because Fx does not allow two instances of the same type - to be present in the container without annotating them. - `NewServeMux` does not know which `Route` to use. Let's fix this. - -5. Annotate `NewEchoHandler` and `NewHelloHandler` in `main()` with names for - both handlers. - - ```go mdox-exec='region ex/get-started/06-another-handler/main.go route-provides' - fx.Annotate( - NewEchoHandler, - fx.As(new(Route)), - fx.ResultTags(`name:"echo"`), - ), - fx.Annotate( - NewHelloHandler, - fx.As(new(Route)), - fx.ResultTags(`name:"hello"`), - ), - ``` - -6. Add another Route parameter to `NewServeMux`. - - ```go mdox-exec='region ex/get-started/06-another-handler/main.go mux' - // NewServeMux builds a ServeMux that will route requests - // to the given routes. - func NewServeMux(route1, route2 Route) *http.ServeMux { - mux := http.NewServeMux() - mux.Handle(route1.Pattern(), route1) - mux.Handle(route2.Pattern(), route2) - return mux - } - ``` - -7. Annotate `NewServeMux` in `main()` to pick these two *names values*. - - ```go mdox-exec='region ex/get-started/06-another-handler/main.go mux-provide' - fx.Provide( - NewHTTPServer, - fx.Annotate( - NewServeMux, - fx.ParamTags(`name:"echo"`, `name:"hello"`), - ), - ``` - -8. Run the program. - - ``` - {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} - {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"name:\\\"echo\\\"\" \"name:\\\"hello\\\"\"])","type":"*http.ServeMux"} - {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"name:\\\"echo\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"echo\"]"} - {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"name:\\\"hello\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"hello\"]"} - {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} - {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} - {"level":"info","msg":"invoking","function":"main.main.func2()"} - {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} - {"level":"info","msg":"Starting HTTP server","addr":":8080"} - {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"56.334µs"} - {"level":"info","msg":"started"} - ``` - -9. Send requests to it. - - ``` - $ curl -X POST -d 'hello' http://localhost:8080/echo - hello - - $ curl -X POST -d 'gopher' http://localhost:8080/hello - Hello, gopher - ``` - -**What did we just do?** - -We added a constructor that produces a value -with the same type as an existing type. -We annotated constructors with `fx.ResultTags` to produce *named values*, -and the consumer with `fx.ParamTags` to consume these named values. diff --git a/docs/get-started/conclusion.md b/docs/get-started/conclusion.md deleted file mode 100644 index 183b8f8d2..000000000 --- a/docs/get-started/conclusion.md +++ /dev/null @@ -1,10 +0,0 @@ -# Conclusion - -This marks the end of this tutorial. -In this tutorial, we covered, - -- how to start an Fx application from scratch -- how to inject new dependencies and modify existing ones -- how to use interfaces to decouple components -- how to use named values -- how to use [value groups](/value-groups.md) diff --git a/docs/get-started/echo-handler.md b/docs/get-started/echo-handler.md deleted file mode 100644 index bba895229..000000000 --- a/docs/get-started/echo-handler.md +++ /dev/null @@ -1,106 +0,0 @@ -# Register a handler - -We built a server that can receive requests, -but it doesn't yet know how to handle them. -Let's fix that. - -1. Define a basic HTTP handler that copies the incoming request body - to the response. - Add the following to the bottom of your file. - - ```go mdox-exec='region ex/get-started/03-echo-handler/main.go echo-handler' - // EchoHandler is an http.Handler that copies its request body - // back to the response. - type EchoHandler struct{} - - // NewEchoHandler builds a new EchoHandler. - func NewEchoHandler() *EchoHandler { - return &EchoHandler{} - } - - // ServeHTTP handles an HTTP request to the /echo endpoint. - func (*EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if _, err := io.Copy(w, r.Body); err != nil { - fmt.Fprintln(os.Stderr, "Failed to handle request:", err) - } - } - ``` - - Provide this to the application. - - ```go mdox-exec='region ex/get-started/03-echo-handler/main.go provide-handler' - fx.Provide( - NewHTTPServer, - NewEchoHandler, - ), - fx.Invoke(func(*http.Server) {}), - ``` - -2. Next, write a function that builds an `*http.ServeMux`. - The `*http.ServeMux` will route requests received by the server to different - handlers. - To begin with, it will route requests sent to `/echo` to `*EchoHandler`, - so its constructor should accept `*EchoHandler` as an argument. - - ```go mdox-exec='region ex/get-started/03-echo-handler/main.go serve-mux' - // NewServeMux builds a ServeMux that will route requests - // to the given EchoHandler. - func NewServeMux(echo *EchoHandler) *http.ServeMux { - mux := http.NewServeMux() - mux.Handle("/echo", echo) - return mux - } - ``` - - Likewise, provide this to the application. - - ```go mdox-exec='region ex/get-started/03-echo-handler/main.go provides' - fx.Provide( - NewHTTPServer, - NewServeMux, - NewEchoHandler, - ), - ``` - - Note that `NewServeMux` was added above `NewEchoHandler`--the order - in which constructors are given to `fx.Provide` does not matter. - -3. Lastly, modify the `NewHTTPServer` function to connect - the server to this `*ServeMux`. - - ```go mdox-exec='region ex/get-started/03-echo-handler/main.go connect-mux' - func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux) *http.Server { - srv := &http.Server{Addr: ":8080", Handler: mux} - lc.Append(fx.Hook{ - ``` - -4. Run the server. - - ``` - [Fx] PROVIDE *http.Server <= main.NewHTTPServer() - [Fx] PROVIDE *http.ServeMux <= main.NewServeMux() - [Fx] PROVIDE *main.EchoHandler <= main.NewEchoHandler() - [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() - [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() - [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() - [Fx] INVOKE main.main.func1() - [Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer) - Starting HTTP server at :8080 - [Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 7.459µs - [Fx] RUNNING - ``` - -5. Send a request to the server. - - ```shell - $ curl -X POST -d 'hello' http://localhost:8080/echo - hello - ``` - -**What did we just do?** - -We added more components with `fx.Provide`. -These components declared dependencies on each other -by adding parameters to their constructors. -Fx will resolve component dependencies by parameters and return values -of the provided functions. diff --git a/docs/get-started/http-server.md b/docs/get-started/http-server.md deleted file mode 100644 index 8eb0c7a32..000000000 --- a/docs/get-started/http-server.md +++ /dev/null @@ -1,135 +0,0 @@ -# Add an HTTP server - -In the previous section, we wrote a minimal Fx application -that doesn't do anything. -Let's add an HTTP server to it. - -1. Write a function to build your HTTP server. - - ```go mdox-exec='region ex/get-started/02-http-server/main.go partial' - // NewHTTPServer builds an HTTP server that will begin serving requests - // when the Fx application starts. - func NewHTTPServer(lc fx.Lifecycle) *http.Server { - srv := &http.Server{Addr: ":8080"} - return srv - } - ``` - - This isn't enough, though--we need to tell Fx how to start the HTTP server. - That's what the additional `fx.Lifecycle` argument is for. - -2. Add a *lifecycle hook* to the application with the `fx.Lifecycle` object. - This tells Fx how to start and stop the HTTP server. - - ```go mdox-exec='region ex/get-started/02-http-server/main.go full' - func NewHTTPServer(lc fx.Lifecycle) *http.Server { - srv := &http.Server{Addr: ":8080"} - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - ln, err := net.Listen("tcp", srv.Addr) - if err != nil { - return err - } - fmt.Println("Starting HTTP server at", srv.Addr) - go srv.Serve(ln) - return nil - }, - OnStop: func(ctx context.Context) error { - return srv.Shutdown(ctx) - }, - }) - return srv - } - ``` - -3. Provide this to your Fx application above with `fx.Provide`. - - ```go mdox-exec='region ex/get-started/02-http-server/main.go provide-server' - func main() { - fx.New( - fx.Provide(NewHTTPServer), - ).Run() - } - ``` - -4. Run the application. - - ``` - [Fx] PROVIDE *http.Server <= main.NewHTTPServer() - [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() - [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() - [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() - [Fx] RUNNING - ``` - - Huh? Did something go wrong? - The first line in the output states that the server was provided, - but it doesn't include our "Starting HTTP server" message. - The server didn't run. - -5. To fix that, add an `fx.Invoke` that requests the constructed server. - - ```go mdox-exec='region ex/get-started/02-http-server/main.go app' - fx.New( - fx.Provide(NewHTTPServer), - fx.Invoke(func(*http.Server) {}), - ).Run() - ``` - -6. Run the application again. - This time we should see "Starting HTTP server" in the output. - - ``` - [Fx] PROVIDE *http.Server <= main.NewHTTPServer() - [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() - [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() - [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() - [Fx] INVOKE main.main.func1() - [Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer) - Starting HTTP server at :8080 - [Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 7.958µs - [Fx] RUNNING - ``` - -7. Send a request to the running server. - - ```shell - $ curl http://localhost:8080 - 404 page not found - ``` - - The request is a 404 because the server doesn't know how to handle it yet. - We'll fix that in the next section. - -8. Stop the application. - - ``` - ^C - [Fx] INTERRUPT - [Fx] HOOK OnStop main.NewHTTPServer.func2() executing (caller: main.NewHTTPServer) - [Fx] HOOK OnStop main.NewHTTPServer.func2() called by main.NewHTTPServer ran successfully in 129.875µs - ``` - -**What did we just do?** - -We used `fx.Provide` to add an HTTP server to the application. -The server hooks into the Fx application lifecycle--it will -start serving requests when we call `App.Run`, -and it will stop running when the application receives a stop signal. -We used `fx.Invoke` to request that the HTTP server is always instantiated, -even if none of the other components in the application reference it directly. - -**Related Resources** - -* [Application lifecycle](/lifecycle.md) further explains what Fx lifecycles are, - and how to use them. - - diff --git a/docs/get-started/logger.md b/docs/get-started/logger.md deleted file mode 100644 index 80c048994..000000000 --- a/docs/get-started/logger.md +++ /dev/null @@ -1,112 +0,0 @@ -# Add a logger - -Our application currently prints -the "Starting HTTP server" message to standard out, -and errors to standard error. -Both, standard out and error are also a form of global state. -We should print to a logger object. - -We'll use [Zap](https://pkg.go.dev/go.uber.org/zap) in this section of the tutorial -but you should be able to use any logging system. - -1. Provide a Zap logger to the application. - In this tutorial, we'll use [`zap.NewExample`](https://pkg.go.dev/go.uber.org/zap#NewExample), - but for real applications, you should use `zap.NewProduction` - or build a more customized logger. - - ```go mdox-exec='region ex/get-started/04-logger/main.go provides' - fx.Provide( - NewHTTPServer, - NewServeMux, - NewEchoHandler, - zap.NewExample, - ), - ``` - -2. Add a field to hold the logger on `EchoHandler`, - and in `NewEchoHandler` add a new logger argument to set this field. - - ```go mdox-exec='region ex/get-started/04-logger/main.go echo-init' - type EchoHandler struct { - log *zap.Logger - } - - func NewEchoHandler(log *zap.Logger) *EchoHandler { - return &EchoHandler{log: log} - } - ``` - -3. In the `EchoHandler.ServeHTTP` method, - use the logger instead of printing to standard error. - - ```go mdox-exec='region ex/get-started/04-logger/main.go echo-serve' - func (h *EchoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if _, err := io.Copy(w, r.Body); err != nil { - h.log.Warn("Failed to handle request", zap.Error(err)) - } - } - ``` - -4. Similarly, update `NewHTTPServer` to expect a logger - and log the "Starting HTTP server" message to that. - - ```go mdox-exec='region ex/get-started/04-logger/main.go http-server' - func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, log *zap.Logger) *http.Server { - srv := &http.Server{Addr: ":8080", Handler: mux} - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - ln, err := net.Listen("tcp", srv.Addr) - if err != nil { - return err - } - log.Info("Starting HTTP server", zap.String("addr", srv.Addr)) - go srv.Serve(ln) - ``` - -5. (**Optional**) You can use the same Zap logger for Fx's own logs as well. - - ```go mdox-exec='region ex/get-started/04-logger/main.go fx-logger' - func main() { - fx.New( - fx.WithLogger(func(log *zap.Logger) fxevent.Logger { - return &fxevent.ZapLogger{Logger: log} - }), - ``` - - This will replace the `[Fx]` messages with messages printed to the logger. - -6. Run the application. - - ``` - {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} - {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"} - {"level":"info","msg":"provided","constructor":"main.NewEchoHandler()","type":"*main.EchoHandler"} - {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} - {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} - {"level":"info","msg":"invoking","function":"main.main.func2()"} - {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} - {"level":"info","msg":"Starting HTTP server","addr":":8080"} - {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"6.292µs"} - {"level":"info","msg":"started"} - ``` - -7. Post a request to it. - - ```shell - $ curl -X POST -d 'hello' http://localhost:8080/echo - hello - ``` - -**What did we just do?** - -We added another component to the application with `fx.Provide`, -and injected that into other components that need to print messages. -To do that, we only had to add a new parameter to the constructors. - -In the optional step, -we told Fx that we'd like to provide a custom logger for Fx's own operations. -We used the existing `fxevent.ZapLogger` to build this custom logger from our -injected logger, so that all logs follow the same format. diff --git a/docs/get-started/many-handlers.md b/docs/get-started/many-handlers.md deleted file mode 100644 index 3e4b81743..000000000 --- a/docs/get-started/many-handlers.md +++ /dev/null @@ -1,103 +0,0 @@ -# Register many handlers - -We added two handlers in the previous section, -but we reference them both explicitly by name when we build `NewServeMux`. -This will quickly become inconvenient if we add more handlers. - -It's preferable if `NewServeMux` doesn't know how many handlers or their names, -and instead just accepts a list of handlers to register. - -Let's do that. - -1. Modify `NewServeMux` to operate on a list of `Route` objects. - - ```go mdox-exec='region ex/get-started/07-many-handlers/main.go mux' - func NewServeMux(routes []Route) *http.ServeMux { - mux := http.NewServeMux() - for _, route := range routes { - mux.Handle(route.Pattern(), route) - } - return mux - } - ``` - -2. Annotate the `NewServeMux` entry in `main` to say - that it accepts a slice that contains the contents of the "routes" group. - - ```go mdox-exec='region ex/get-started/07-many-handlers/main.go mux-provide' - fx.Provide( - NewHTTPServer, - fx.Annotate( - NewServeMux, - fx.ParamTags(`group:"routes"`), - ), - ``` - -3. Define a new function `AsRoute` to build functions that feed into this - group. - - ```go mdox-exec='region ex/get-started/07-many-handlers/main.go AsRoute' - // AsRoute annotates the given constructor to state that - // it provides a route to the "routes" group. - func AsRoute(f any) any { - return fx.Annotate( - f, - fx.As(new(Route)), - fx.ResultTags(`group:"routes"`), - ) - } - ``` - -4. Wrap the `NewEchoHandler` and `NewHelloHandler` constructors in `main()` - with `AsRoute` so that they feed their routes into this group. - - ```go mdox-exec='region ex/get-started/07-many-handlers/main.go route-provides' - fx.Provide( - AsRoute(NewEchoHandler), - AsRoute(NewHelloHandler), - zap.NewExample, - ), - ``` - -5. Finally, run the application. - - ``` - {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} - {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"group:\\\"routes\\\"\"])","type":"*http.ServeMux"} - {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"} - {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"} - {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} - {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} - {"level":"info","msg":"invoking","function":"main.main.func2()"} - {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} - {"level":"info","msg":"Starting HTTP server","addr":":8080"} - {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"5µs"} - {"level":"info","msg":"started"} - ``` - -6. Send requests to it. - - ``` - $ curl -X POST -d 'hello' http://localhost:8080/echo - hello - - $ curl -X POST -d 'gopher' http://localhost:8080/hello - Hello, gopher - ``` - -**What did we just do?** - -We annotated `NewServeMux` to consume a *value group* as a slice, -and we annotated our existing handler constructors to feed into this value -group. -Any other constructor in the application can also feed values -into this value group as long as the result conforms to the `Route` interface. -They will all be collected together and passed into our `ServeMux` constructor. - -**Related Resources** - -* [Value groups](/value-groups.md) further explains what value groups are, - and how to use them. diff --git a/docs/get-started/registration.md b/docs/get-started/registration.md deleted file mode 100644 index fdbd1639d..000000000 --- a/docs/get-started/registration.md +++ /dev/null @@ -1,93 +0,0 @@ -# Decouple registration - -`NewServeMux` above declares an explicit dependency on `EchoHandler`. -This is an unnecessarily tight coupling. -Does the `ServeMux` really need to know the *exact* handler implementation? -If we want to write tests for `ServeMux`, -we shouldn't have to construct an `EchoHandler`. - -Let's try to fix this. - -1. Define a `Route` type in your main.go. - This is an extension of `http.Handler` where the handler knows its - registration path. - - ```go mdox-exec='region ex/get-started/05-registration/main.go route' - // Route is an http.Handler that knows the mux pattern - // under which it will be registered. - type Route interface { - http.Handler - - // Pattern reports the path at which this is registered. - Pattern() string - } - ``` - -2. Modify `EchoHandler` to implement this interface. - - ```go mdox-exec='region ex/get-started/05-registration/main.go echo-pattern' - func (*EchoHandler) Pattern() string { - return "/echo" - } - ``` - -3. In `main()`, annotate the `NewEchoHandler` entry to state that the handler - should be provided as a Route. - - ```go mdox-exec='region ex/get-started/05-registration/main.go provides' - fx.Provide( - NewHTTPServer, - NewServeMux, - fx.Annotate( - NewEchoHandler, - fx.As(new(Route)), - ), - zap.NewExample, - ), - ``` - -4. Modify `NewServeMux` to accept a Route and use its provided pattern. - - ```go mdox-exec='region ex/get-started/05-registration/main.go mux' - // NewServeMux builds a ServeMux that will route requests - // to the given Route. - func NewServeMux(route Route) *http.ServeMux { - mux := http.NewServeMux() - mux.Handle(route.Pattern(), route) - return mux - } - ``` - -5. Run the service. - - ``` - {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} - {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"} - {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]])","type":"main.Route"} - {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} - {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} - {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} - {"level":"info","msg":"invoking","function":"main.main.func2()"} - {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} - {"level":"info","msg":"Starting HTTP server","addr":":8080"} - {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"10.125µs"} - {"level":"info","msg":"started"} - ``` - -6. Send a request to it. - - ```shell - $ curl -X POST -d 'hello' http://localhost:8080/echo - hello - ``` - -**What did we just do?** - -We introduced an interface to decouple the implementation -from the consumer. -We then [annotated](../annotate.md) a previously provided constructor -with `fx.Annotate` and `fx.As` -to [cast its result to that interface](../annotate.md#casting-structs-to-interfaces). -This way, `NewEchoHandler` was able to continue returning an `*EchoHandler`. diff --git a/docs/index.md b/docs/index.md deleted file mode 100755 index 4a4c165cb..000000000 --- a/docs/index.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -tagline: Dependency injection system for Go. -home: true -footer: Made by Uber with ❤️ -features: - - details: | - Fx helps you remove global state from your application. - No more init() or global variables. - Use Fx-managed singletons. - title: Eliminate globals - - details: | - Fx lets teams within your organization build loosely-coupled - and well-integrated shareable components. - title: Code reuse - - details: | - Fx is the backbone of nearly all Go services at Uber. - title: Battle-tested -actionText: Get Started → -actionLink: /get-started/ ---- - - diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 000000000..383f22451 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,171 @@ +site_name: Fx +site_url: https://uber-go.github.io/fx +site_description: >- + A dependency injection system for Go. + +repo_url: https://github.com/uber-go/fx +repo_name: uber-go/fx +edit_uri: edit/master/docs/src/ + +# .md files reside inside the src directory. +docs_dir: src + +# The generated site will be placed in the _site directory. +# This is the default for GitHub Pages' upload-artifact action. +site_dir: _site + +extra: + analytics: + provider: google + property: G-4YWLTPJ46M + +# Treat all warnings as errors. +strict: true + +# By default, mkdocs will turn "foo/bar.md" into "foo/bar/index.html", +# linking to it as "foo/bar/". +# This does not match what we were using previously (foo/bar.html). +# So we'll disable this behavior. +use_directory_urls: false + +validation: + # Warn about Markdown files not listed in the nav. + omitted_files: warn + + # If a link is /foo/bar.md, + # turn it into relative to the src/ directory. + absolute_links: relative_to_docs + + # Warn about broken internal links to pages or anchors. + unrecognized_links: warn + anchors: warn + +theme: + name: material + + # Support dark and light mode. + palette: + - scheme: default + toggle: + icon: material/toggle-switch + name: Switch to dark mode + - scheme: slate + toggle: + icon: material/toggle-switch-off-outline + name: Switch to light mode + + features: + - content.action.edit # show an 'edit this page' button + - content.code.copy # show 'copy' button on code blocks + - content.tooltips # render alt text as tooltips + - header.autohide # hide header on scroll + - navigation.footer # show next/prev page footer + - navigation.indexes # allow foo/index.md to be home for foo/ + - navigation.instant # use SPA-style navigation + - navigation.instant.progress + # show loading progress for instant nav + - search.suggest # show search suggestions + - toc.follow # highlight current section in TOC + - toc.integrate # merge TOC into nav sidebar + +plugins: + # Downloads third-party assets at build time and bundles them with the site. + # This avoids calling out to third-party servers when the site is viewed. + # We'll do this only if the build is for 'master' + - privacy: + enabled: !ENV [MASTER_BUILD, false] + + # Enable search + - search + + # Show Created/Modified dates + - git-revision-date-localized: + enabled: !ENV [CI, false] + enable_creation_date: true + fallback_to_build_date: true + + # Redirect old links to new ones. + - redirects: + redirect_maps: + value-groups.md: value-groups/index.md + + +markdown_extensions: + - admonition # admonitions (info/warning/error/etc.) + - attr_list # custom HTML attributes for Markdown elements + - def_list # definition lists + - md_in_html # HTML blocks tagged with Markdown contents + - pymdownx.details # collapsible blocks + + # snippets enables including code snippets from other files + # with the "--8<--" syntax. + # + # It will search for snippets in the provided base paths. + # We put code samples in the "ex/" directory, so that's one of the base paths. + - pymdownx.snippets: + base_path: [ex] + + # Syntax-highlighting of code fences (```), + # plus custom fences for Mermaid diagrams. + - pymdownx.superfences: + # Mermaid diagram support. + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + + # Tabbed content blocks, e.g. "Language A" vs "Language B". + - pymdownx.tabbed: + alternate_style: true # recommended + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower + + # GitHub-style task lists. + - pymdownx.tasklist: + custom_checkbox: true # recommended + + # Generate a TOC for all pages with a permalink for all headers. + - toc: + permalink: true + +nav: + - Home: index.md + - Get started: + - get-started/index.md + - get-started/minimal.md + - get-started/http-server.md + - get-started/echo-handler.md + - get-started/logger.md + - get-started/registration.md + - get-started/another-handler.md + - get-started/many-handlers.md + - get-started/conclusion.md + - intro.md + - Concepts: + - Container: container.md + - Lifecycle: lifecycle.md + - Modules: modules.md + - Features: + - Parameter Objects: parameter-objects.md + - Result Objects: result-objects.md + - Annotations: annotate.md + - Value groups: + - value-groups/index.md + - value-groups/feed.md + - value-groups/consume.md + - FAQ: faq.md + - Community: + - Contributing: contributing.md + - Release notes: changelog.md + - API Reference: https://pkg.go.dev/go.uber.org/fx + +# Pages that are not listed in the nav must be listed here. +# not_in_nav: | +# get-started/*.md + + +# Also watch ex/ for changes +# as that's where we store snippets. +watch: + - ex diff --git a/docs/package.json b/docs/package.json deleted file mode 100755 index a34f1908a..000000000 --- a/docs/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "Fx", - "version": "0.0.1", - "description": "Dependency injection system for Go.", - "main": "index.js", - "authors": { - "name": "Uber Technologies", - "email": "oss@uber.com" - }, - "repository": "https://github.com/uber-go/fx", - "scripts": { - "dev": "vuepress dev", - "build": "vuepress build" - }, - "license": "MIT", - "devDependencies": { - "@vuepress/plugin-back-to-top": "^1.9.7", - "@vuepress/plugin-medium-zoom": "^1.9.7", - "vuepress": "^1.5.3", - "vuepress-plugin-fulltext-search": "^2.2.1", - "vuepress-plugin-mermaidjs": "1.9.1" - }, - "dependencies": { - "cacheable-request": "10.2.7", - "got": "^10.3.0", - "vuepress-plugin-code-copy": "^1.0.6" - } -} diff --git a/docs/pyproject.toml b/docs/pyproject.toml new file mode 100644 index 000000000..783e53de4 --- /dev/null +++ b/docs/pyproject.toml @@ -0,0 +1,9 @@ +[tool.uv] +dev-dependencies = [ + "mkdocs-material>=9.5.33", + "mkdocs-git-revision-date-localized-plugin>=1.2.7", + "mkdocs>=1.6.0", + "mkdocs-redirects>=1.2.1", +] +[tool.uv.workspace] +members = [] diff --git a/docs/annotate.md b/docs/src/annotate.md similarity index 56% rename from docs/annotate.md rename to docs/src/annotate.md index 356e0558f..43c852183 100644 --- a/docs/annotate.md +++ b/docs/src/annotate.md @@ -29,34 +29,24 @@ A function that: 1. Given a function that you're passing to `fx.Provide`, `fx.Invoke`, or `fx.Decorate`, - ```go mdox-exec='region ex/annotate/sample.go before' - fx.Provide( - NewHTTPClient, - ), - ``` + ```go + --8<-- "annotate/sample.go:before" + ``` 2. Wrap the function with `fx.Annotate`. - ```go mdox-exec='region ex/annotate/sample.go wrap' - fx.Provide( - fx.Annotate( - NewHTTPClient, - ), - ), - ``` + ```go + --8<-- "annotate/sample.go:wrap-1" + --8<-- "annotate/sample.go:wrap-2" + ``` 3. Inside `fx.Annotate`, pass in your annotations. - ```go mdox-exec='region ex/annotate/sample.go annotate' - fx.Provide( - fx.Annotate( - NewHTTPClient, - fx.ResultTags(`name:"client"`), - ), - ), - ``` + ```go + --8<-- "annotate/sample.go:annotate" + ``` - This annotation tags the result of the function with a name. + This annotation tags the result of the function with a name. **Related resources** @@ -72,57 +62,42 @@ into an interface consumed by another function. 1. A function that produces a struct or pointer value. - ```go mdox-exec='region ex/annotate/cast.go constructor' - func NewHTTPClient(Config) (*http.Client, error) { - ``` + ```go + --8<-- "annotate/cast.go:constructor" + ``` 2. A function that consumes the result of the producer. - ```go mdox-exec='region ex/annotate/cast_bad.go struct-consumer' - func NewGitHubClient(client *http.Client) *github.Client { - ``` + ```go + --8<-- "annotate/cast_bad.go:struct-consumer" + ``` 3. Both functions are provided to the Fx application. - ```go mdox-exec='region ex/annotate/cast_bad.go provides' - fx.Provide( - NewHTTPClient, - NewGitHubClient, - ), - ``` + ```go + --8<-- "annotate/cast_bad.go:provides" + ``` **Steps** 1. Declare an interface that matches the API of the produced `*http.Client`. - ```go mdox-exec='region ex/annotate/cast.go interface' - type HTTPClient interface { - Do(*http.Request) (*http.Response, error) - } - - // This is a compile-time check that verifies - // that our interface matches the API of http.Client. - var _ HTTPClient = (*http.Client)(nil) - ``` + ```go + --8<-- "annotate/cast.go:interface" + ``` 2. Change the consumer to accept the interface instead of the struct. - ```go mdox-exec='region ex/annotate/cast.go iface-consumer' - func NewGitHubClient(client HTTPClient) *github.Client { - ``` + ```go + --8<-- "annotate/cast.go:iface-consumer" + ``` 3. Finally, annotate the producer with `fx.As` to state that it produces an interface value. - ```go mdox-exec='region ex/annotate/cast.go provides' - fx.Provide( - fx.Annotate( - NewHTTPClient, - fx.As(new(HTTPClient)), - ), - NewGitHubClient, - ), - ``` + ```go + --8<-- "annotate/cast.go:provides" + ``` With this change, diff --git a/docs/src/changelog.md b/docs/src/changelog.md new file mode 120000 index 000000000..699cc9e7b --- /dev/null +++ b/docs/src/changelog.md @@ -0,0 +1 @@ +../../CHANGELOG.md \ No newline at end of file diff --git a/docs/container.md b/docs/src/container.md similarity index 73% rename from docs/container.md rename to docs/src/container.md index affabd686..2e3327988 100644 --- a/docs/container.md +++ b/docs/src/container.md @@ -32,29 +32,29 @@ Fx provides two ways to provide values to the container: - `fx.Provide` for values that have a constructor. - ```go - fx.Provide( - func(cfg *Config) *Logger { /* ... */ }, - ) - ``` + ```go + fx.Provide( + func(cfg *Config) *Logger { /* ... */ }, + ) + ``` - This says that Fx should use this function to construct a `*Logger`, - and that a `*Config` is required to build one. + This says that Fx should use this function to construct a `*Logger`, + and that a `*Config` is required to build one. - `fx.Supply` for pre-built non-interface values. - ```go - fx.Provide( - fx.Supply(&Config{ - Name: "my-app", - }), - ) - ``` + ```go + fx.Provide( + fx.Supply(&Config{ + Name: "my-app", + }), + ) + ``` - This says that Fx should use the provided `*Config` as-is. + This says that Fx should use the provided `*Config` as-is. - **Important**: `fx.Supply` is only for non-interface values. - See *When to use fx.Supply* for more details. + **Important**: `fx.Supply` is only for non-interface values. + See *When to use fx.Supply* for more details. Values provided to the container are available to all other constructors. In the example above, the `*Config` would become available to the `*Logger` constructor, @@ -76,27 +76,24 @@ fx.Supply(&Config{Name: "my-app"}) However, even then, `fx.Supply` comes with a caveat: it can only be used for non-interface values. -
- Why can't I use fx.Supply for interface values? +??? question "Why can't I use fx.Supply for interface values?" -This is a technical limitation imposed by the fact that `fx.Supply` has to rely -on runtime reflection to determine the type of the value. + This is a technical limitation imposed by the fact that `fx.Supply` has to rely + on runtime reflection to determine the type of the value. -Passing an interface value to `fx.Supply` is a lossy operation: -it loses the original interface type, only giving us `interface{}`, -at which point reflection will only reveal the concrete type of the value. + Passing an interface value to `fx.Supply` is a lossy operation: + it loses the original interface type, only giving us `interface{}`, + at which point reflection will only reveal the concrete type of the value. -For example, consider: + For example, consider: -```go -var svc RepositoryService = &repoService{ ... } -``` - -If you were to pass `svc` to `fx.Supply`, -the container would only know that it's a `*repoService`, -and it will not know that you intend to use it as a `RepositoryService`. + ```go + var svc RepositoryService = &repoService{ ... } + ``` -
+ If you were to pass `svc` to `fx.Supply`, + the container would only know that it's a `*repoService`, + and it will not know that you intend to use it as a `RepositoryService`. ## Using values diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 120000 index 000000000..f939e75f2 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1 @@ +../../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/faq.md b/docs/src/faq.md similarity index 62% rename from docs/faq.md rename to docs/src/faq.md index 133d7fff3..33b87356f 100644 --- a/docs/faq.md +++ b/docs/src/faq.md @@ -13,44 +13,44 @@ Ordering of options relative to each other is as follows: Operations like `fx.Provide` and `fx.Supply` are run in dependency order. Dependencies are determined by the function parameters and results. - ```go - // The following are all equivalent: - fx.Options(fx.Provide(ParseConfig, NewLogger)) - fx.Options(fx.Provide(NewLogger, ParseConfig)) - fx.Options(fx.Provide(ParseConfig), fx.Provide(NewLogger)) - fx.Options(fx.Provide(NewLogger), fx.Provide(ParseConfig)) - ``` + ```go + // The following are all equivalent: + fx.Options(fx.Provide(ParseConfig, NewLogger)) + fx.Options(fx.Provide(NewLogger, ParseConfig)) + fx.Options(fx.Provide(ParseConfig), fx.Provide(NewLogger)) + fx.Options(fx.Provide(NewLogger), fx.Provide(ParseConfig)) + ``` * Consuming values: Operations like `fx.Invoke` and `fx.Populate` are run after their dependencies have been satisfied: after `fx.Provide`s. - Relative to each other, invokes are run in the order they were specified. + Relative to each other, invokes are run in the order they were specified. - ```go - fx.Invoke(a, b) - // a() is run before b() - ``` + ```go + fx.Invoke(a, b) + // a() is run before b() + ``` - `fx.Module` hierarchies affect invocation order: - invocations in a parent module are run after those of a child module. + `fx.Module` hierarchies affect invocation order: + invocations in a parent module are run after those of a child module. - ```go - fx.Options( - fx.Invoke(a), - fx.Module("child", fx.Invoke(b)), - ), - // b() is run before a() - ``` + ```go + fx.Options( + fx.Invoke(a), + fx.Module("child", fx.Invoke(b)), + ), + // b() is run before a() + ``` * Replacing values: Operations like `fx.Decorate` and `fx.Replace` are run after the Provide operations that they depend on, but before the Invoke operations that consume those values. - Ordering of decorations relative to each other - is determined by `fx.Module` hierarchies: - decorations in a parent module are applied after those of a child module. + Ordering of decorations relative to each other + is determined by `fx.Module` hierarchies: + decorations in a parent module are applied after those of a child module. ## Why does `fx.Supply` not accept interfaces? diff --git a/docs/src/get-started/another-handler.md b/docs/src/get-started/another-handler.md new file mode 100644 index 000000000..dab0fe4fd --- /dev/null +++ b/docs/src/get-started/another-handler.md @@ -0,0 +1,107 @@ +# Register another handler + +The handler we defined above has a single handler. +Let's add another. + +1. Build a new handler in the same file. + + ```go + --8<-- "get-started/06-another-handler/main.go:hello-init" + ``` + +2. Implement the `Route` interface for this handler. + + ```go + --8<-- "get-started/06-another-handler/main.go:hello-methods-1" + --8<-- "get-started/06-another-handler/main.go:hello-methods-2" + ``` + + The handler reads its request body, + and writes a welcome message back to the caller. + +3. Provide this to the application as a `Route` next to `NewEchoHandler`. + + ```go + --8<-- "get-started/06-another-handler/main.go:hello-provide-partial-1" + --8<-- "get-started/06-another-handler/main.go:hello-provide-partial-2" + --8<-- "get-started/06-another-handler/main.go:hello-provide-partial-3" + ``` + +4. Run the application--the service will fail to start. + + ``` + [Fx] PROVIDE *http.Server <= main.NewHTTPServer() + [Fx] PROVIDE *http.ServeMux <= main.NewServeMux() + [Fx] PROVIDE main.Route <= fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]]) + [Fx] Error after options were applied: fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from: + [...] + [Fx] ERROR Failed to start: the following errors occurred: + - fx.Provide(fx.Annotate(main.NewHelloHandler(), fx.As([[main.Route]])) from: + [...] + Failed: cannot provide function "main".NewHelloHandler ([..]/main.go:53): cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80) + ``` + + That's a lot of output, but inside the error message, we see: + + ``` + cannot provide main.Route from [0].Field0: already provided by "main".NewEchoHandler ([..]/main.go:80) + ``` + + This fails because Fx does not allow two instances of the same type + to be present in the container without annotating them. + `NewServeMux` does not know which `Route` to use. Let's fix this. + +5. Annotate `NewEchoHandler` and `NewHelloHandler` in `main()` with names for + both handlers. + + ```go + --8<-- "get-started/06-another-handler/main.go:route-provides" + ``` + +6. Add another Route parameter to `NewServeMux`. + + ```go + --8<-- "get-started/06-another-handler/main.go:mux" + ``` + +7. Annotate `NewServeMux` in `main()` to pick these two *names values*. + + ```go + --8<-- "get-started/06-another-handler/main.go:mux-provide" + ``` + +8. Run the program. + + ``` + {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} + {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"name:\\\"echo\\\"\" \"name:\\\"hello\\\"\"])","type":"*http.ServeMux"} + {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"name:\\\"echo\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"echo\"]"} + {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"name:\\\"hello\\\"\"]), fx.As([[main.Route]])","type":"main.Route[name = \"hello\"]"} + {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} + {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} + {"level":"info","msg":"invoking","function":"main.main.func2()"} + {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} + {"level":"info","msg":"Starting HTTP server","addr":":8080"} + {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"56.334µs"} + {"level":"info","msg":"started"} + ``` + +9. Send requests to it. + + ``` + $ curl -X POST -d 'hello' http://localhost:8080/echo + hello + + $ curl -X POST -d 'gopher' http://localhost:8080/hello + Hello, gopher + ``` + +**What did we just do?** + +We added a constructor that produces a value +with the same type as an existing type. +We annotated constructors with `fx.ResultTags` to produce *named values*, +and the consumer with `fx.ParamTags` to consume these named values. diff --git a/docs/src/get-started/conclusion.md b/docs/src/get-started/conclusion.md new file mode 100644 index 000000000..50a638788 --- /dev/null +++ b/docs/src/get-started/conclusion.md @@ -0,0 +1,10 @@ +# Conclusion + +This marks the end of this tutorial. +In this tutorial, we covered, + +- [x] how to start an Fx application from scratch +- [x] how to inject new dependencies and modify existing ones +- [x] how to use interfaces to decouple components +- [x] how to use named values +- [x] how to use [value groups](/value-groups/index.md) diff --git a/docs/src/get-started/echo-handler.md b/docs/src/get-started/echo-handler.md new file mode 100644 index 000000000..067fefe7a --- /dev/null +++ b/docs/src/get-started/echo-handler.md @@ -0,0 +1,77 @@ +# Register a handler + +We built a server that can receive requests, +but it doesn't yet know how to handle them. +Let's fix that. + +1. Define a basic HTTP handler that copies the incoming request body + to the response. + Add the following to the bottom of your file. + + ```go + --8<-- "get-started/03-echo-handler/main.go:echo-handler" + ``` + + Provide this to the application. + + ```go + --8<-- "get-started/03-echo-handler/main.go:provide-handler-1" + --8<-- "get-started/03-echo-handler/main.go:provide-handler-2" + ``` + +2. Next, write a function that builds an `*http.ServeMux`. + The `*http.ServeMux` will route requests received by the server to different + handlers. + To begin with, it will route requests sent to `/echo` to `*EchoHandler`, + so its constructor should accept `*EchoHandler` as an argument. + + ```go + --8<-- "get-started/03-echo-handler/main.go:serve-mux" + ``` + + Likewise, provide this to the application. + + ```go + --8<-- "get-started/03-echo-handler/main.go:provides" + ``` + + Note that `NewServeMux` was added above `NewEchoHandler`--the order + in which constructors are given to `fx.Provide` does not matter. + +3. Lastly, modify the `NewHTTPServer` function to connect + the server to this `*ServeMux`. + + ```go + --8<-- "get-started/03-echo-handler/main.go:connect-mux" + ``` + +4. Run the server. + + ``` + [Fx] PROVIDE *http.Server <= main.NewHTTPServer() + [Fx] PROVIDE *http.ServeMux <= main.NewServeMux() + [Fx] PROVIDE *main.EchoHandler <= main.NewEchoHandler() + [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() + [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() + [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() + [Fx] INVOKE main.main.func1() + [Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer) + Starting HTTP server at :8080 + [Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 7.459µs + [Fx] RUNNING + ``` + +5. Send a request to the server. + + ```shell + $ curl -X POST -d 'hello' http://localhost:8080/echo + hello + ``` + +**What did we just do?** + +We added more components with `fx.Provide`. +These components declared dependencies on each other +by adding parameters to their constructors. +Fx will resolve component dependencies by parameters and return values +of the provided functions. diff --git a/docs/src/get-started/http-server.md b/docs/src/get-started/http-server.md new file mode 100644 index 000000000..e8e3e9bde --- /dev/null +++ b/docs/src/get-started/http-server.md @@ -0,0 +1,109 @@ +# Add an HTTP server + +In the previous section, we wrote a minimal Fx application +that doesn't do anything. +Let's add an HTTP server to it. + +1. Write a function to build your HTTP server. + + ```go + --8<-- "get-started/02-http-server/main.go:partial-1" + --8<-- "get-started/02-http-server/main.go:partial-2" + ``` + + This isn't enough, though--we need to tell Fx how to start the HTTP server. + That's what the additional `fx.Lifecycle` argument is for. + +2. Add a *lifecycle hook* to the application with the `fx.Lifecycle` object. + This tells Fx how to start and stop the HTTP server. + + ```go + --8<-- "get-started/02-http-server/main.go:full" + ``` + +3. Provide this to your Fx application above with `fx.Provide`. + + ```go + --8<-- "get-started/02-http-server/main.go:provide-server-1" + --8<-- "get-started/02-http-server/main.go:provide-server-2" + ``` + +4. Run the application. + + ``` + [Fx] PROVIDE *http.Server <= main.NewHTTPServer() + [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() + [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() + [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() + [Fx] RUNNING + ``` + + Huh? Did something go wrong? + The first line in the output states that the server was provided, + but it doesn't include our "Starting HTTP server" message. + The server didn't run. + +5. To fix that, add an `fx.Invoke` that requests the constructed server. + + ```go + --8<-- "get-started/02-http-server/main.go:app" + ``` + +6. Run the application again. + This time we should see "Starting HTTP server" in the output. + + ``` + [Fx] PROVIDE *http.Server <= main.NewHTTPServer() + [Fx] PROVIDE fx.Lifecycle <= go.uber.org/fx.New.func1() + [Fx] PROVIDE fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm() + [Fx] PROVIDE fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm() + [Fx] INVOKE main.main.func1() + [Fx] HOOK OnStart main.NewHTTPServer.func1() executing (caller: main.NewHTTPServer) + Starting HTTP server at :8080 + [Fx] HOOK OnStart main.NewHTTPServer.func1() called by main.NewHTTPServer ran successfully in 7.958µs + [Fx] RUNNING + ``` + +7. Send a request to the running server. + + ```shell + $ curl http://localhost:8080 + 404 page not found + ``` + + The request is a 404 because the server doesn't know how to handle it yet. + We'll fix that in the next section. + +8. Stop the application. + + ``` + ^C + [Fx] INTERRUPT + [Fx] HOOK OnStop main.NewHTTPServer.func2() executing (caller: main.NewHTTPServer) + [Fx] HOOK OnStop main.NewHTTPServer.func2() called by main.NewHTTPServer ran successfully in 129.875µs + ``` + +**What did we just do?** + +We used `fx.Provide` to add an HTTP server to the application. +The server hooks into the Fx application lifecycle--it will +start serving requests when we call `App.Run`, +and it will stop running when the application receives a stop signal. +We used `fx.Invoke` to request that the HTTP server is always instantiated, +even if none of the other components in the application reference it directly. + +**Related Resources** + +* [Application lifecycle](/lifecycle.md) further explains what Fx lifecycles are, + and how to use them. + + + diff --git a/docs/get-started/README.md b/docs/src/get-started/index.md similarity index 99% rename from docs/get-started/README.md rename to docs/src/get-started/index.md index 2ea29ef82..29a9cdec2 100644 --- a/docs/get-started/README.md +++ b/docs/src/get-started/index.md @@ -1,5 +1,6 @@ # Get started with Fx + This introduces you to the basics of Fx. In this tutorial you will: diff --git a/docs/src/get-started/logger.md b/docs/src/get-started/logger.md new file mode 100644 index 000000000..527d2c287 --- /dev/null +++ b/docs/src/get-started/logger.md @@ -0,0 +1,85 @@ +# Add a logger + +Our application currently prints +the "Starting HTTP server" message to standard out, +and errors to standard error. +Both, standard out and error are also a form of global state. +We should print to a logger object. + +We'll use [Zap](https://pkg.go.dev/go.uber.org/zap) in this section of the tutorial +but you should be able to use any logging system. + +1. Provide a Zap logger to the application. + In this tutorial, we'll use [`zap.NewExample`](https://pkg.go.dev/go.uber.org/zap#NewExample), + but for real applications, you should use `zap.NewProduction` + or build a more customized logger. + + ```go + --8<-- "get-started/04-logger/main.go:provides" + ``` + +2. Add a field to hold the logger on `EchoHandler`, + and in `NewEchoHandler` add a new logger argument to set this field. + + ```go + --8<-- "get-started/04-logger/main.go:echo-init-1" + --8<-- "get-started/04-logger/main.go:echo-init-2" + ``` + +3. In the `EchoHandler.ServeHTTP` method, + use the logger instead of printing to standard error. + + ```go + --8<-- "get-started/04-logger/main.go:echo-serve" + ``` + +4. Similarly, update `NewHTTPServer` to expect a logger + and log the "Starting HTTP server" message to that. + + ```go + --8<-- "get-started/04-logger/main.go:http-server" + ``` + +5. (**Optional**) You can use the same Zap logger for Fx's own logs as well. + + ```go + --8<-- "get-started/04-logger/main.go:fx-logger" + ``` + + This will replace the `[Fx]` messages with messages printed to the logger. + +6. Run the application. + + ``` + {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} + {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"} + {"level":"info","msg":"provided","constructor":"main.NewEchoHandler()","type":"*main.EchoHandler"} + {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} + {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} + {"level":"info","msg":"invoking","function":"main.main.func2()"} + {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} + {"level":"info","msg":"Starting HTTP server","addr":":8080"} + {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"6.292µs"} + {"level":"info","msg":"started"} + ``` + +7. Post a request to it. + + ```shell + $ curl -X POST -d 'hello' http://localhost:8080/echo + hello + ``` + +**What did we just do?** + +We added another component to the application with `fx.Provide`, +and injected that into other components that need to print messages. +To do that, we only had to add a new parameter to the constructors. + +In the optional step, +we told Fx that we'd like to provide a custom logger for Fx's own operations. +We used the existing `fxevent.ZapLogger` to build this custom logger from our +injected logger, so that all logs follow the same format. diff --git a/docs/src/get-started/many-handlers.md b/docs/src/get-started/many-handlers.md new file mode 100644 index 000000000..86b8e0712 --- /dev/null +++ b/docs/src/get-started/many-handlers.md @@ -0,0 +1,81 @@ +# Register many handlers + +We added two handlers in the previous section, +but we reference them both explicitly by name when we build `NewServeMux`. +This will quickly become inconvenient if we add more handlers. + +It's preferable if `NewServeMux` doesn't know how many handlers or their names, +and instead just accepts a list of handlers to register. + +Let's do that. + +1. Modify `NewServeMux` to operate on a list of `Route` objects. + + ```go + --8<-- "get-started/07-many-handlers/main.go:mux" + ``` + +2. Annotate the `NewServeMux` entry in `main` to say + that it accepts a slice that contains the contents of the "routes" group. + + ```go + --8<-- "get-started/07-many-handlers/main.go:mux-provide" + ``` + +3. Define a new function `AsRoute` to build functions that feed into this + group. + + ```go + --8<-- "get-started/07-many-handlers/main.go:AsRoute" + ``` + +4. Wrap the `NewEchoHandler` and `NewHelloHandler` constructors in `main()` + with `AsRoute` so that they feed their routes into this group. + + ```go + --8<-- "get-started/07-many-handlers/main.go:route-provides-1" + --8<-- "get-started/07-many-handlers/main.go:route-provides-2" + ``` + +5. Finally, run the application. + + ``` + {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} + {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewServeMux(), fx.ParamTags([\"group:\\\"routes\\\"\"])","type":"*http.ServeMux"} + {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"} + {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewHelloHandler(), fx.ResultTags([\"group:\\\"routes\\\"\"]), fx.As([[main.Route]])","type":"main.Route[group = \"routes\"]"} + {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} + {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} + {"level":"info","msg":"invoking","function":"main.main.func2()"} + {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} + {"level":"info","msg":"Starting HTTP server","addr":":8080"} + {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"5µs"} + {"level":"info","msg":"started"} + ``` + +6. Send requests to it. + + ``` + $ curl -X POST -d 'hello' http://localhost:8080/echo + hello + + $ curl -X POST -d 'gopher' http://localhost:8080/hello + Hello, gopher + ``` + +**What did we just do?** + +We annotated `NewServeMux` to consume a *value group* as a slice, +and we annotated our existing handler constructors to feed into this value +group. +Any other constructor in the application can also feed values +into this value group as long as the result conforms to the `Route` interface. +They will all be collected together and passed into our `ServeMux` constructor. + +**Related Resources** + +* [Value groups](/value-groups/index.md) further explains what value groups are, + and how to use them. diff --git a/docs/get-started/minimal.md b/docs/src/get-started/minimal.md similarity index 89% rename from docs/get-started/minimal.md rename to docs/src/get-started/minimal.md index 182ccb8b3..bd5423bb5 100644 --- a/docs/get-started/minimal.md +++ b/docs/src/get-started/minimal.md @@ -5,14 +5,8 @@ This application won't do anything yet except print a bunch of logs. 1. Write a minimal `main.go`. - ```go mdox-exec='region ex/get-started/01-minimal/main.go main' - package main - - import "go.uber.org/fx" - - func main() { - fx.New().Run() - } + ```go + --8<-- "get-started/01-minimal/main.go:main" ``` 2. Run the application. diff --git a/docs/src/get-started/registration.md b/docs/src/get-started/registration.md new file mode 100644 index 000000000..6737d5b4d --- /dev/null +++ b/docs/src/get-started/registration.md @@ -0,0 +1,70 @@ +# Decouple registration + +`NewServeMux` above declares an explicit dependency on `EchoHandler`. +This is an unnecessarily tight coupling. +Does the `ServeMux` really need to know the *exact* handler implementation? +If we want to write tests for `ServeMux`, +we shouldn't have to construct an `EchoHandler`. + +Let's try to fix this. + +1. Define a `Route` type in your main.go. + This is an extension of `http.Handler` where the handler knows its + registration path. + + ```go + --8<-- "get-started/05-registration/main.go:route" + ``` + +2. Modify `EchoHandler` to implement this interface. + + ```go + --8<-- "get-started/05-registration/main.go:echo-pattern" + ``` + +3. In `main()`, annotate the `NewEchoHandler` entry to state that the handler + should be provided as a Route. + + ```go + --8<-- "get-started/05-registration/main.go:provides" + ``` + +4. Modify `NewServeMux` to accept a Route and use its provided pattern. + + ```go + --8<-- "get-started/05-registration/main.go:mux" + ``` + +5. Run the service. + + ``` + {"level":"info","msg":"provided","constructor":"main.NewHTTPServer()","type":"*http.Server"} + {"level":"info","msg":"provided","constructor":"main.NewServeMux()","type":"*http.ServeMux"} + {"level":"info","msg":"provided","constructor":"fx.Annotate(main.NewEchoHandler(), fx.As([[main.Route]])","type":"main.Route"} + {"level":"info","msg":"provided","constructor":"go.uber.org/zap.NewExample()","type":"*zap.Logger"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.New.func1()","type":"fx.Lifecycle"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).shutdowner-fm()","type":"fx.Shutdowner"} + {"level":"info","msg":"provided","constructor":"go.uber.org/fx.(*App).dotGraph-fm()","type":"fx.DotGraph"} + {"level":"info","msg":"initialized custom fxevent.Logger","function":"main.main.func1()"} + {"level":"info","msg":"invoking","function":"main.main.func2()"} + {"level":"info","msg":"OnStart hook executing","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer"} + {"level":"info","msg":"Starting HTTP server","addr":":8080"} + {"level":"info","msg":"OnStart hook executed","callee":"main.NewHTTPServer.func1()","caller":"main.NewHTTPServer","runtime":"10.125µs"} + {"level":"info","msg":"started"} + ``` + +6. Send a request to it. + + ```shell + $ curl -X POST -d 'hello' http://localhost:8080/echo + hello + ``` + +**What did we just do?** + +We introduced an interface to decouple the implementation +from the consumer. +We then [annotated](../annotate.md) a previously provided constructor +with `fx.Annotate` and `fx.As` +to [cast its result to that interface](../annotate.md#casting-structs-to-interfaces). +This way, `NewEchoHandler` was able to continue returning an `*EchoHandler`. diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 000000000..4d2ecc1ae --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,24 @@ +# Fx + +Fx is **a dependency injection system for Go**. + +
+ +- **Eliminate globals** + + Fx helps you remove global state from your application. + No more `init()` or global variables. + Use Fx-managed singletons. + +- **Code reuse** + + Fx lets teams within your organization build loosely-coupled + and well-integrated shareable components. + +- **Battle-tested** + + Fx is the backbone of nearly all Go services at Uber. + +
+ +[Get started](get-started/index.md){ .md-button .md-button--primary } diff --git a/docs/intro.md b/docs/src/intro.md similarity index 84% rename from docs/intro.md rename to docs/src/intro.md index 8ae33002c..ec7d47ca4 100644 --- a/docs/intro.md +++ b/docs/src/intro.md @@ -9,4 +9,4 @@ With Fx you can: - build general purpose shareable modules that just work If this is your first time with Fx, -check out our [getting started tutorial](get-started/). +check out our [getting started tutorial](get-started/index.md). diff --git a/docs/lifecycle.md b/docs/src/lifecycle.md similarity index 99% rename from docs/lifecycle.md rename to docs/src/lifecycle.md index e52f6aba2..735bee274 100644 --- a/docs/lifecycle.md +++ b/docs/src/lifecycle.md @@ -27,7 +27,7 @@ flowchart LR Start --> Wait --> Stop end Invoke --> Start - + style Wait stroke-dasharray: 5 5 ``` diff --git a/docs/modules.md b/docs/src/modules.md similarity index 68% rename from docs/modules.md rename to docs/src/modules.md index 41f28e045..57053d759 100644 --- a/docs/modules.md +++ b/docs/src/modules.md @@ -14,67 +14,49 @@ 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", - ``` + ```go + --8<-- "modules/module.go:start" + ``` 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, - ), - ) - ``` + ```go + --8<-- + modules/module.go:start + modules/module.go:provide + --8<-- + ``` 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), - ) - ``` + ```go + --8<-- + modules/module.go:start + modules/module.go:invoke + --8<-- + ``` 4. 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), - - ) - ``` + ```go + --8<-- + modules/module.go:start + modules/module.go:decorate + --8<-- + ``` 5. Lastly, if you want to keep a constructor's outputs contained to your module (and modules your module includes), you can add an `fx.Private` when providing. - ```go mdox-exec='region ex/modules/module.go private' - var Module = fx.Module("server", - fx.Provide( - New, - ), - fx.Provide( - fx.Private, - parseConfig, - ), - fx.Invoke(startServer), - fx.Decorate(wrapLogger), - - ) - ``` + ```go + --8<-- + modules/module.go:start + modules/module.go:privateProvide + --8<-- + ``` In this case, `parseConfig` is now private to the "server" module. No modules that contain "server" will be able to use the resulting @@ -125,19 +107,13 @@ 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) { +```go +--8<-- "modules/module.go:start" +--8<-- "modules/module.go:provide" +--8<-- "modules/module.go:privateProvide" +--8<-- "modules/module.go:endProvide" +--8<-- "modules/module.go:config" +--8<-- "modules/module.go:new" ``` In this example, we don't export `parseConfig`, @@ -150,17 +126,15 @@ 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 +??? example "Bad: No way to build the server without Fx" -```go -var Module = fx.Module("server", - fx.Provide(newServer), -) - -func newServer(...) (*Server, error) -``` + ```go + var Module = fx.Module("server", + fx.Provide(newServer), + ) -::: + func newServer(...) (*Server, error) + ``` ### Use parameter objects @@ -168,15 +142,9 @@ 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) { +```go +--8<-- "modules/module.go:params" +--8<-- "modules/module.go:new" ``` **Rationale**: @@ -185,13 +153,11 @@ 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) -``` +??? example "Bad: Cannot add new parameters without breaking" -::: + ```go + func New(log *zap.Logger) (Result, error) + ``` ### Use result objects @@ -199,14 +165,9 @@ 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) { +```go +--8<-- "modules/module.go:result" +--8<-- "modules/module.go:new" ``` **Rationale**: @@ -215,13 +176,11 @@ 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 +??? example "Bad: Cannot add new results without breaking" -```go -func New(Params) (*Server, error) -``` - -::: + ```go + func New(Params) (*Server, error) + ``` ### Don't provide what you don't own @@ -235,33 +194,29 @@ 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 +??? example "Bad: Provides a dependency" -type Result struct { - fx.Out + ```go + package httpfx - Client *http.Client - Logger *zap.Logger // BAD -} -``` + type Result struct { + fx.Out -::: + Client *http.Client + Logger *zap.Logger // BAD + } + ``` -::: details Bad: Bundles another module +??? example "Bad: Bundles another module" -```go -package httpfx + ```go + package httpfx -var Module = fx.Module("http", - fx.Provide(New), - zapfx.Module, // BAD -) -``` - -::: + var Module = fx.Module("http", + fx.Provide(New), + zapfx.Module, // BAD + ) + ``` **Exception**: Organization or team-level "kitchen sink" modules @@ -283,35 +238,31 @@ it should not have the "fx" suffix in its name. 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 +??? example "Good: Business logic consumes net/http.Client" -```go -package httpfx + ```go + package httpfx -import "net/http" + import "net/http" -type Result struct { - fx.Out + type Result struct { + fx.Out - Client *http.Client -} -``` + Client *http.Client + } + ``` -::: +??? example "Bad: Fx module implements logger" -::: details Bad: Fx module implements logger + ```go + package logfx -```go -package logfx - -type Logger struct { - // ... -} - -func New(...) Logger -``` + type Logger struct { + // ... + } -::: + func New(...) Logger + ``` ### Invoke sparingly diff --git a/docs/parameter-objects.md b/docs/src/parameter-objects.md similarity index 58% rename from docs/parameter-objects.md rename to docs/src/parameter-objects.md index 38484a56e..8e7682ff4 100644 --- a/docs/parameter-objects.md +++ b/docs/src/parameter-objects.md @@ -26,45 +26,34 @@ To use parameter objects in Fx, take the following steps: If the constructor is named `New`, name the struct `Params`. This naming isn't strictly necessary, but it's a good convention to follow. - ```go mdox-exec='region ex/parameter-objects/define.go empty' - type ClientParams struct { - } - ``` + ```go + --8<-- "parameter-objects/define.go:empty-1" + --8<-- "parameter-objects/define.go:empty-2" + ``` 2. Embed `fx.In` into this struct. - ```go mdox-exec='region ex/parameter-objects/define.go fxin' - type ClientParams struct { - fx.In - ``` + ```go + --8<-- "parameter-objects/define.go:fxin" + ``` 3. Add this new type as a parameter to your constructor *by value*. - ```go mdox-exec='region ex/parameter-objects/define.go takeparam' - func NewClient(p ClientParams) (*Client, error) { - ``` + ```go + --8<-- "parameter-objects/define.go:takeparam" + ``` 4. Add dependencies of your constructor as **exported** fields on this struct. - ```go mdox-exec='region ex/parameter-objects/define.go fields' - type ClientParams struct { - fx.In - - Config ClientConfig - HTTPClient *http.Client - } - ``` + ```go + --8<-- "parameter-objects/define.go:fields" + ``` 5. Consume these fields in your constructor. - ```go mdox-exec='region ex/parameter-objects/define.go consume' - func NewClient(p ClientParams) (*Client, error) { - return &Client{ - url: p.Config.URL, - http: p.HTTPClient, - // ... - }, nil - ``` + ```go + --8<-- "parameter-objects/define.go:consume" + ``` Once you have a parameter object on a function, you can use it to access other advanced features of Fx: @@ -84,39 +73,23 @@ the new fields must be **optional**. 1. Take an existing parameter object. - ```go mdox-exec='region ex/parameter-objects/extend.go start' - type Params struct { - fx.In - - Config ClientConfig - HTTPClient *http.Client - } - - func New(p Params) (*Client, error) { - ``` + ```go + --8<-- "parameter-objects/extend.go:start-1" + --8<-- "parameter-objects/extend.go:start-2" + --8<-- "parameter-objects/extend.go:start-3" + ``` 2. Add a new field to it for your new dependency and **mark it optional** to keep this change backwards compatible. - ```go mdox-exec='region ex/parameter-objects/extend.go full' - type Params struct { - fx.In - - Config ClientConfig - HTTPClient *http.Client - Logger *zap.Logger `optional:"true"` - } - ``` + ```go + --8<-- "parameter-objects/extend.go:full" + ``` 3. In your constructor, consume this field. Be sure to handle the case when this field is absent -- it will take the zero value of its type in that case. - ```go mdox-exec='region ex/parameter-objects/extend.go consume' - func New(p Params) (*Client, error) { - log := p.Logger - if log == nil { - log = zap.NewNop() - } - // ... - ``` + ```go + --8<-- "parameter-objects/extend.go:consume" + ``` diff --git a/docs/result-objects.md b/docs/src/result-objects.md similarity index 56% rename from docs/result-objects.md rename to docs/src/result-objects.md index c63259bb2..082c4ea58 100644 --- a/docs/result-objects.md +++ b/docs/src/result-objects.md @@ -25,45 +25,35 @@ To use result objects in Fx, take the following steps: If the constructor is named `New`, name the struct `Result`. This naming isn't strictly necessary, but it's a good convention to follow. - ```go mdox-exec='region ex/result-objects/define.go empty' - type ClientResult struct { - } - ``` + ```go + --8<-- "result-objects/define.go:empty-1" + --8<-- "result-objects/define.go:empty-2" + ``` 2. Embed `fx.Out` into this struct. - ```go mdox-exec='region ex/result-objects/define.go fxout' - type ClientResult struct { - fx.Out - ``` + ```go + --8<-- "result-objects/define.go:fxout" + ``` 3. Use this new type as the return value of your constructor *by value*. - ```go mdox-exec='region ex/result-objects/define.go returnresult' - func NewClient() (ClientResult, error) { - ``` + ```go + --8<-- "result-objects/define.go:returnresult" + ``` 4. Add values produced by your constructor as **exported** fields on this struct. - ```go mdox-exec='region ex/result-objects/define.go fields' - type ClientResult struct { - fx.Out - - Client *Client - } - ``` + ```go + --8<-- "result-objects/define.go:fields" + ``` 5. Set these fields and return an instance of this struct from your constructor. - ```go mdox-exec='region ex/result-objects/define.go produce' - func NewClient() (ClientResult, error) { - client := &Client{ - // ... - } - return ClientResult{Client: client}, nil - } - ``` + ```go + --8<-- "result-objects/define.go:produce" + ```