confx

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 7 Imported by: 0

README

confx

Go Reference CI Release

Explicit confmaker registration for Uber Fx. confx provides typed configs to Fx consumers. confmaker owns defaults, ENV parsing, validation and diagnostics; the adapter owns wiring them into Fx.

Install · Quick start · Instances · Lifecycle · Diagnostics · Testing · Reference

Installation

Requires Go 1.27 or later. The command below installs the tagged release.

go get github.com/uchaloop/confx@v0.2.0
import "github.com/uchaloop/confx"

Import github.com/uchaloop/confmaker when using core options such as WithEnv, WithPrefix or WithDump.

Quick start

package main

import (
	"fmt"
	"log"

	"github.com/uchaloop/confx"
	"go.uber.org/fx"
)

type ServerConfig struct {
	Port int `env:"PORT"`
}

func (c *ServerConfig) SetDefaults() { c.Port = 8080 }

func main() {
	app := fx.New(
		confx.Module(),
		confx.Provide[ServerConfig]("server"),
		fx.Invoke(func(cfg ServerConfig) {
			fmt.Println(cfg.Port) // Pass the config to your server constructor.
		}),
	)
	if err := app.Err(); err != nil {
		log.Fatal(err)
	}
	app.Run()
}

Run with no ENV for the default 8080, or set SERVER_PORT in your IDE or shell. The config type needs no dependency on either confmaker or Fx.

flowchart LR
    A["Provide / ProvideNamed"] --> B["Module: one core loader"]
    B --> C["Register and load all configs during fx.New"]
    C -->|Success| D["Typed values for consumers"]
    C -->|Error| E["app.Err; startup prevented"]

Multiple instances

An instance name, an ENV prefix and an Fx tag serve different purposes:

Concept Source Purpose
Instance name First argument to Provide / ProvideNamed Core registration identity and diagnostics
ENV prefix Derived from the name, or WithPrefix Variable lookup
Fx name tag ProvideNamed only Select a named dependency

Using ServerConfig from the quick start, replace its registration with:

confx.Provide[ServerConfig]("server"),
confx.ProvideNamed[ServerConfig]("replica", confmaker.WithPrefix("READ_SERVER_")),

The first reads SERVER_PORT, the second READ_SERVER_PORT. Consume them with:

type Params struct {
	fx.In
	Primary ServerConfig
	Replica ServerConfig `name:"replica"`
}

Use fx.Invoke(func(p Params) { /* use p.Primary and p.Replica */ }) or take Params in a constructor. Two untagged providers of the same type conflict in Fx even if their instance names differ. WithPrefix never changes the Fx tag.

Lifecycle and errors

Include exactly one confx.Module() or confx.FromLoader(loader). All provided configs load during fx.New, including those with no consumers. A core loading error is available through app.Err() and prevents startup.

Core structured errors remain reachable through Fx's wrappers:

if err := app.Err(); err != nil {
	for _, problem := range confmaker.ConfigErrors(err) {
		fmt.Println(problem.Kind, problem.InstanceName, problem.VariableName)
	}
	log.Fatal(err)
}

Always handle the original error: missing Module and Fx dependency conflicts are not core ConfigError diagnostics. Config values should be treated as read-only. Provider options may be reused across applications; each app receives its own loader and values.

Load diagnostics

Use the core handler option to process a value-free report before Fx receives a configuration loading error, while keeping fx.New(...).Run():

fx.New(
    confx.Module(
        confmaker.WithDiagnosticHandler(func(report confmaker.LoadReport) {
            // Format the report with your application's logger.
        }),
    ),
    confx.Provide[ServerConfig]("server"),
).Run()

The handler runs once during configuration loading in fx.New, on success or error. It receives sources, statuses and structured categories, without values or error messages. Loading panics propagate and skip the handler. No automatic logging or additional Fx adapter is involved.

For explicit inspection after app := fx.New(...), create diagnostics := confmaker.MakeDiagnostics(), pass confmaker.WithDiagnostics(diagnostics) to Module, and read diagnostics.Report() before calling Run. Reading it after Run is unsuitable for startup errors because Fx may terminate the process.

In external-loader mode, pass these options to confmaker.MakeLoader and use FromLoader as usual. If Fx rejects the graph or registration mode before loading begins, there is no load report: the receiver remains LoadNotStarted and the handler is not called. Inspect app.Err() for those Fx errors. Each Diagnostics receiver belongs to one loader; use a fresh receiver for each ordinary Module app.

Testing

Pass an isolated ENV map to Module. This setup uses ServerConfig above and imports testing, confmaker, confx and Fx:

func TestServerConfig(t *testing.T) {
	t.Parallel()

	var got ServerConfig
	app := fx.New(
		fx.NopLogger,
		confx.Module(confmaker.WithEnv(map[string]string{"SERVER_PORT": "9090"})),
		confx.Provide[ServerConfig]("server"),
		fx.Populate(&got),
	)
	if err := app.Err(); err != nil {
		t.Fatal(err)
	}
	if got.Port != 9090 {
		t.Fatalf("unexpected port: %d", got.Port)
	}
}

No app.Start is needed to inspect configuration values: loading already happened during construction. In a real app, unrelated constructors can run during fx.New; this is not a general-purpose dry-run mode.

External loader and manifest

The simple Module + Provide API remains the default. For documentation generation without reading ENV or constructing services, register configurations in the core before creating Fx, then bridge their handles into the application:

package main

import (
    "flag"
    "log"
    "os"

    "github.com/uchaloop/confmaker"
    "github.com/uchaloop/confx"
    "go.uber.org/fx"
)

type ServerConfig struct {
    Port int `env:"PORT,required"`
}

func main() {
    describe := flag.Bool("describe", false, "Print configuration documentation")
    flag.Parse()

    loader := confmaker.MakeLoader()
    server := loader.Register[ServerConfig]("server")

    if *describe {
        if err := loader.WriteManifestMarkdown(os.Stdout); err != nil {
            log.Fatal(err)
        }
        return
    }

    fx.New(
        confx.FromLoader(loader),
        confx.FromHandle(server),
        fx.Invoke(func(cfg ServerConfig) { log.Printf("server port: %d", cfg.Port) }),
        // Add runtime modules here.
    ).Run()
}

go run . -describe completes before fx.New; required ENV is not needed. Normal startup loads during fx.New. Either Run or explicit Start/Stop is valid: separating description from application construction is what matters.

Adapter Purpose
FromLoader(loader) Use an existing loader instead of Module
FromHandle(handle) Provide an existing registration as an untagged value
FromHandleNamed(handle) Provide it with its registration name as the Fx tag

All registrations load, even when no handle has a consumer. Handles must belong to the supplied loader; nil, zero and foreign handles fail construction. An already-loaded loader reuses its result. Reusing an external loader across apps also shares its values and load state; create separate loaders for isolation.

The two modes cannot be mixed: Module accepts only Provide/ProvideNamed; FromLoader accepts only FromHandle/FromHandleNamed. Mode and handle ownership checks finish before any registration or loading. Invalid connections leave the external loader unchanged, so a corrected connection can reuse it. Once Load has run, its result remains final as usual.

Register every config outside Fx in the extended mode. The manifest generated before fx.New then describes the complete registration set.

For export formats and guarantees, see the core manifest guide. The adapters do not parse flags or terminate the process.

Reference

Acknowledgements

Thanks to the authors and maintainers of Uber Fx for its dependency injection and lifecycle primitives.

License

MIT

Documentation

Overview

Package confx adapts github.com/uchaloop/confmaker to Uber Fx. The application explicitly registers its package configs; confmaker owns ENV parsing, defaults, validation and diagnostics, while confx provides the resulting typed values.

Import github.com/uchaloop/confx for registration and github.com/uchaloop/confmaker when using core options such as WithEnv or WithPrefix.

Registration

fx.New(
	confx.Module(),
	confx.Provide[postgres.Config]("postgres"),
	confx.ProvideNamed[postgres.Config]("replica", confmaker.WithPrefix("REPLICA_POSTGRES_")),
)

Module creates one confmaker.Loader per application and is required once when configs are registered, unless FromLoader supplies an existing loader instead. Provide passes the explicit instance name to the core and provides the result without an Fx tag. ProvideNamed additionally uses that name as the Fx tag name:"<name>". confmaker validates names, derives ENV prefixes and loads the configurations; the adapter handles their registration in Fx.

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:

type Params struct {
	fx.In
	Primary postgres.Config
	Replica postgres.Config `name:"replica"`
}

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.

Load diagnostics

Pass confmaker.WithDiagnosticHandler to Module to process a value-free report before a loading error reaches Fx, preserving fx.New(...).Run(). For explicit inspection, pass confmaker.WithDiagnostics and read the receiver 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 the receiver stays not started.

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.

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"
	"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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func FromHandle added in v0.3.0

func FromHandle[T any](handle *confmaker.Handle[T]) fx.Option

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

func FromHandleNamed[T any](handle *confmaker.Handle[T]) fx.Option

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

func FromLoader(loader *confmaker.Loader) fx.Option

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.EnvOption) 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.

func Provide

func Provide[T any](name string, opts ...confmaker.ConfigOption) fx.Option

Provide registers a config with the required instance name and provides it without an Fx tag. confmaker validates the name and derives its default 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

func ProvideNamed[T any](name string, opts ...confmaker.ConfigOption) fx.Option

ProvideNamed registers a config with the required instance name and provides it with the Fx tag name:"<name>". The name also 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"
	"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",
	})

	type params struct {
		fx.In

		Primary StoreConfig
		Replica StoreConfig `name:"replica"`
	}

	app := fx.New(
		fx.NopLogger,
		confx.Module(env),
		confx.Provide[StoreConfig]("store"),
		confx.ProvideNamed[StoreConfig]("replica", confmaker.WithPrefix("REPLICA_STORE_")),
		fx.Invoke(func(p params) {
			fmt.Println(p.Primary.Host, p.Replica.Host)
		}),
	)
	if err := app.Err(); err != nil {
		fmt.Println(err)
	}

}
Output:
db:5432 replica:5432

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL