Documentation
¶
Overview ¶
Package confx adapts github.com/uchaloop/confmaker/v2 to Uber Fx. The application explicitly registers its package configs; confmaker owns configuration engines, defaults, validation and diagnostics, while confx provides the resulting typed values. The adapter supports both the built-in ENV engine and application-supplied engines.
Import github.com/uchaloop/confx for registration and github.com/uchaloop/confmaker/v2 when using core options such as WithEnv or WithPrefix.
Quick start ¶
Declare configuration in the domain and place ordinary Fx options in Runtime:
var Module = confx.MakeModule("store",
confx.Provide[Config]("store"),
confx.Runtime(fx.Provide(makeService)),
)
Compose domain modules once:
modules := confx.MakeModules(store.Module, orders.Module) fx.New(modules.FX()).Run()
FX installs one core adapter per application. Do not add Module or FromLoader alongside it. WithOptions returns a new composition with shared core loader options; each Fx application gets a fresh loader.
Registration and instances ¶
Provide supplies an untagged value; ProvideNamed uses the registration name as its Fx name tag. With the built-in engine, the name also determines the ENV prefix; WithPrefix overrides only that prefix. A custom engine owns source lookup and tags. The original Module plus Provide API remains available for ordinary Fx composition when offline manifest collection is not needed.
Lifecycle ¶
Every provided config is loaded during fx.New, including configs with no consumer. Loading errors are available from fx.App.Err and prevent startup. Options returned by Provide and ProvideNamed may be reused across applications; each application has its own loader and config values.
Multiple instances and errors ¶
Use Provide for an untagged value. Two values of the same Go type need distinct Fx identities, even when their ENV prefixes differ. ProvideNamed uses the instance name as that identity:
fx.Invoke(func(params struct {
fx.In
Primary postgres.Config
Replica postgres.Config `name:"replica"`
}) {
fmt.Println(params.Primary.Host, params.Replica.Host)
})
Inspect construction errors with app.Err(). Core ConfigError diagnostics remain reachable through confmaker.ConfigErrors and errors.As. Missing Module or conflicting Fx providers are adapter or Fx errors, not core config diagnostics. Loaded configs should be treated as read-only, including their maps and slices.
External registrations ¶
FromLoader uses a caller-owned confmaker.Loader. FromHandle and FromHandleNamed provide existing handles without registering them again. A named handle uses Handle.Name as its Fx tag. All handles must belong to the same loader. Exactly one Module or FromLoader may be installed.
For offline documentation, register configs externally and generate the manifest before fx.New. Normal startup can then use Run or explicit Start/Stop. Module accepts only Provide/ProvideNamed, while FromLoader accepts only FromHandle/FromHandleNamed. Mixing modes or supplying a foreign handle fails before registrations or loading and leaves the external loader unchanged. Previously loaded loaders retain their results. Reusing an external loader across apps shares its load state and values. The adapter does not interpret command-line flags or exit the process.
Custom engines ¶
Module accepts confmaker.LoaderOption, including confmaker.WithEngine. An external loader can select the same backend. Every app has its own loader, but reusing a Module option shares the supplied Engine object, which must support concurrent calls across loaders. Its resources remain caller-owned. Loading still occurs at fx.New; this adapter adds no context or backend resource lifecycle. Custom engines provide config-level reports and do not support Loader.Manifest. Names accepted by the core remain subject to Fx named-tag restrictions. Runnable env/v11 and koanf + TOML applications live in the separate examples/engines module. Their dependencies are not part of this package. ExampleModule_customEngine demonstrates a standard-library JSON decoder.
Load diagnostics ¶
Pass confmaker.WithDiagnosticHandler through WithOptions or Module for a value-free report before a loading error reaches Fx, preserving fx.New(...).Run(). For explicit inspection, enable confmaker.WithDiagnostics() on an external loader and read loader.Report after fx.New, before Run. External loaders accept the same core options. If Fx rejects the graph before loading, the handler is not called and an enabled loader report stays in the not_started state.
Module composition and offline description ¶
MakeModule preserves configuration declarations beside a real Fx module. MakeModules composes one tree for Manifest and FX; WithOptions returns a copy with shared core loader options. FX installs Module itself and creates a fresh loader per application. Do not combine it with Module or FromLoader.
Wrap service providers, invocations and decorators in Runtime. Manifest rejects opaque options outside Runtime with ErrOpaqueOption, before executing defaults, and returns no partial result. Runtime asserts that its contents have no config registrations; configurations hidden in opaque Fx wrappers cannot be discovered. Nested MakeModule preserves module scopes. Manifest runs no Fx constructors or invocations; IncludeDefaults still runs user defaults and renderers. It neither validates the Fx graph nor all load-only options. Go argument evaluation and init are outside this guarantee. Custom engines do not support Manifest.
Core library ¶
The core also works without Fx:
cfg, err := confmaker.Load[postgres.Config]("postgres")
Declaring configs, options, the manifest and loading rules are documented in package github.com/uchaloop/confmaker/v2.
Example ¶
Provide makes the config available to constructors. WithEnv stands in for the process environment here; an application calls confx.Module() without it.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker/v2"
"github.com/uchaloop/confx"
"go.uber.org/fx"
)
// StoreConfig is the kind of config a library declares: plain fields, env tags,
// and no knowledge of where the values come from.
type StoreConfig struct {
Host string `env:"HOST,notEmpty"`
Timeout time.Duration `env:"TIMEOUT"`
}
func (c *StoreConfig) SetDefaults() { c.Timeout = 30 * time.Second }
func main() {
env := confmaker.WithEnv(map[string]string{"STORE_HOST": "db:5432"})
app := fx.New(
fx.NopLogger,
confx.Module(env),
confx.Provide[StoreConfig]("store"),
fx.Invoke(func(cfg StoreConfig) {
fmt.Println(cfg.Host, cfg.Timeout)
}),
)
if err := app.Err(); err != nil {
fmt.Println(err)
}
}
Output: db:5432 30s
Index ¶
- Variables
- func FromHandle[T any](handle *confmaker.Handle[T]) fx.Option
- func FromHandleNamed[T any](handle *confmaker.Handle[T]) fx.Option
- func FromLoader(loader *confmaker.Loader) fx.Option
- func Module(opts ...confmaker.LoaderOption) fx.Option
- func Provide[T any](name string, opts ...confmaker.ConfigOption) fx.Option
- func ProvideNamed[T any](name string, opts ...confmaker.ConfigOption) fx.Option
- func Runtime(opts ...fx.Option) fx.Option
- type ModuleSpec
- type Modules
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrOpaqueOption = errors.New("confx: opaque Fx option")
ErrOpaqueOption means Manifest cannot inspect an option. Use MakeModule for configuration modules and Runtime for options containing no registrations.
Functions ¶
func FromHandle ¶ added in v0.3.0
FromHandle provides an existing registration as an untagged T. FromLoader must reference its owner. No second registration is made. Nil, zero and foreign handles fail app construction before loading the application's configs.
func FromHandleNamed ¶ added in v0.3.0
FromHandleNamed provides an existing registration with its registration name as the Fx name tag. WithPrefix does not affect this name. FromLoader is required.
func FromLoader ¶ added in v0.3.0
FromLoader uses an existing loader instead of Module. Use only FromHandle or FromHandleNamed with it; Provide registrations are rejected before loading. Load runs during fx.New, even without config consumers. A previously loaded loader retains its result and refuses new registrations. Include exactly one Module or FromLoader per app. Sharing an external loader across apps shares its values and one-shot load state.
func Module ¶
func Module(opts ...confmaker.LoaderOption) fx.Option
Module loads the configs registered by Provide and ProvideNamed through one confmaker.Loader per application, configured by opts. Loading happens once during fx.New, when a constructor or invocation requests a config. Every registration is checked, including unused configs. Loading errors reach fx.App.Err and prevent startup, regardless of registration order.
Include exactly one Module or FromLoader. Module also checks its options when no configs are registered.
Example (CustomEngine) ¶
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/uchaloop/confmaker/v2"
"github.com/uchaloop/confx"
"go.uber.org/fx"
)
func main() {
type config struct {
Port int `json:"port"`
}
engine := confmaker.EngineFunc(func(ctx context.Context, req confmaker.LoadRequest) error {
if err := ctx.Err(); err != nil {
return err
}
if req.Name != "server" {
return fmt.Errorf("unknown configuration %q", req.Name)
}
return json.Unmarshal([]byte(`{"port":9090}`), req.Target)
})
app := fx.New(
fx.NopLogger,
confx.Module(confmaker.WithEngine(engine)),
confx.Provide[config]("server"),
fx.Invoke(func(cfg config) {
fmt.Println(cfg.Port)
}),
)
if err := app.Err(); err != nil {
fmt.Println(err)
}
}
Output: 9090
func Provide ¶
Provide registers a config with the required instance name and provides it without an Fx tag. With the built-in engine, confmaker validates the name and derives its ENV prefix; confmaker.WithPrefix overrides only the prefix. Module is required; FromLoader cannot be mixed with Provide. The config is loaded during fx.New even when no constructor consumes it.
func ProvideNamed ¶
ProvideNamed registers a config with the required instance name and provides it with the Fx tag name:"<name>". With the built-in engine, the name determines the default prefix; confmaker.WithPrefix can override the prefix. Module is required, and loading happens during fx.New as for Provide.
Example ¶
ProvideNamed provides a second instance of the same type under an Fx tag, with its own prefix.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker/v2"
"github.com/uchaloop/confx"
"go.uber.org/fx"
)
// StoreConfig is the kind of config a library declares: plain fields, env tags,
// and no knowledge of where the values come from.
type StoreConfig struct {
Host string `env:"HOST,notEmpty"`
Timeout time.Duration `env:"TIMEOUT"`
}
func (c *StoreConfig) SetDefaults() { c.Timeout = 30 * time.Second }
func main() {
env := confmaker.WithEnv(map[string]string{
"STORE_HOST": "db:5432",
"REPLICA_STORE_HOST": "replica:5432",
})
app := fx.New(
fx.NopLogger,
confx.Module(env),
confx.Provide[StoreConfig]("store"),
confx.ProvideNamed[StoreConfig]("replica", confmaker.WithPrefix("REPLICA_STORE_")),
fx.Invoke(func(p struct {
fx.In
Primary StoreConfig
Replica StoreConfig `name:"replica"`
}) {
fmt.Println(p.Primary.Host, p.Replica.Host)
}),
)
if err := app.Err(); err != nil {
fmt.Println(err)
}
}
Output: db:5432 replica:5432
func Runtime ¶ added in v0.5.1
Runtime marks options as containing no configuration declarations. Manifest skips them; Fx applies them normally in their original module scope. Known direct configuration options and nil options are rejected. Registrations hidden inside opaque fx.Options or fx.Module cannot be detected: callers must not hide them here. Argument evaluation and package init run normally in Go.
Types ¶
type ModuleSpec ¶ added in v0.5.1
type ModuleSpec struct {
// contains filtered or unexported fields
}
ModuleSpec preserves configuration declarations alongside a real Fx module. Create it with MakeModule. Its zero value is invalid.
func MakeModule ¶ added in v0.5.1
func MakeModule(name string, opts ...fx.Option) *ModuleSpec
MakeModule groups declarations and runtime options, preserving Fx scopes. Manifest rejects opaque options unless explicitly enclosed in Runtime.
type Modules ¶ added in v0.5.1
type Modules struct {
// contains filtered or unexported fields
}
Modules is an immutable application composition. Use MakeModules to create it. A zero Modules describes an empty application.
func MakeModules ¶ added in v0.5.1
MakeModules captures one module list for Manifest and FX.
Example ¶
One module tree is used for offline description and normal Fx construction.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker/v2"
"github.com/uchaloop/confx"
"go.uber.org/fx"
)
// StoreConfig is the kind of config a library declares: plain fields, env tags,
// and no knowledge of where the values come from.
type StoreConfig struct {
Host string `env:"HOST,notEmpty"`
Timeout time.Duration `env:"TIMEOUT"`
}
func (c *StoreConfig) SetDefaults() { c.Timeout = 30 * time.Second }
func main() {
orders := confx.MakeModule("orders",
confx.Provide[StoreConfig]("store"),
confx.Runtime(
fx.Invoke(func(cfg StoreConfig) {
fmt.Println("loaded:", cfg.Host)
}),
),
)
modules := confx.MakeModules(orders).WithOptions(
confmaker.WithEnv(map[string]string{"STORE_HOST": "db:5432"}),
)
manifest, err := modules.Manifest()
if err != nil {
fmt.Println(err)
return
}
fmt.Println("described:", manifest.Configs[0].InstanceName)
app := fx.New(fx.NopLogger, modules.FX())
if err := app.Err(); err != nil {
fmt.Println(err)
}
}
Output: described: store loaded: db:5432
func (*Modules) FX ¶ added in v0.5.1
FX builds an Fx option with one fresh loader per application. Do not combine it with Module or FromLoader. Ordinary opaque Fx options work in this path.
func (*Modules) Manifest ¶ added in v0.5.1
func (m *Modules) Manifest(opts ...confmaker.ManifestOption) (confmaker.ManifestResult, error)
Manifest describes all visible declarations without constructing an Fx app. It first rejects opaque branches, then delegates to the core. IncludeDefaults executes user defaults and renderers. This does not validate the Fx graph or all load-only options. A custom engine returns ErrManifestUnsupported.
func (*Modules) WithOptions ¶ added in v0.5.1
func (m *Modules) WithOptions(opts ...confmaker.LoaderOption) *Modules
WithOptions returns a copy with additional shared core options. Engine objects and callback closures are shared; the loader itself is never shared.