confmaker

module
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT

README

confmaker

CI Go Reference

Typed configuration for Go services, read from the environment and nowhere else (12factor III). Each library, each package and the application itself declares the config it needs as a plain struct; confmaker fills it, validates it, and hands it to Uber Fx.

  • Defaults in code, where the library's tests and callers see the same values a deployment starts from - not in a tag only the loader can read.
  • A misspelled variable fails the start, with a suggestion, instead of quietly leaving a field at its default.
  • What a declaration gets wrong is refused when it is bound - on the first start, whether or not the environment happens to set that variable.
  • A manifest of every variable the application reads, so a .env.example or a config map is generated from the types themselves.
  • Two dependencies: Fx and a secret type.
go get github.com/uchaloop/confmaker

Quick start

A library declares what it needs and reads nothing:

type Config struct {
	Host     string        `env:"HOST,notEmpty"`
	Timeout  time.Duration `env:"TIMEOUT"`
	Password secret.Secret `env:"PASSWORD"`
}

func (c *Config) SetDefaults() { c.Timeout = 30 * time.Second }

func (c Config) Validate() error {
	if c.Timeout <= 0 {
		return fmt.Errorf("timeout must be positive, got %s", c.Timeout)
	}

	return nil
}

The application names the instance, which gives the environment prefix:

fx.New(
	confx.Module(),

	confx.Provide[store.Config]("store"),       // STORE_HOST, STORE_TIMEOUT, ...
	confx.ProvideNamed[store.Config]("replica"),// REPLICA_HOST, REPLICA_TIMEOUT, ...
	confx.Provide[app.Config]("app"),           // APP_SHUTDOWN_WAIT, ...

	store.Module,
)

Provide gives the container an untagged value, the single default instance; ProvideNamed gives it a value tagged name:"replica", so a repository can ask for either. Nothing else in the process reads the environment.

What it catches

confx.Module() compares the environment against what the Provide calls declare:

unknown configuration variable "STORE_HSOT" (did you mean "STORE_HOST"?)

It has the whole application in view, so it also refuses two instances that would read one prefix, or one variable, and could not then be told apart.

Everything a declaration itself can get wrong is refused before any value is read - a default written in a tag, an option the tag does not define, two fields claiming one variable, envPrefix on anything but a struct nested by value, a secret in a slice or a map, a config nested through a pointer or a slice, a field of a type that cannot be read from text.

Every problem is reported at once, one per line, each naming the config it belongs to:

config "store": required variable "STORE_HOST" is not set
config "store": timeout must be positive

What it prints

confx.Module(confx.WithDump(os.Stdout)) writes every variable the application reads, the value it carries and where that value comes from:

INSTANCE  VARIABLE            TYPE           VALUE          SOURCE
store     STORE_HOST          string         store:9000     env
store     STORE_PASSWORD      secret.Secret  (set)          env
store     STORE_TIMEOUT       time.Duration  30s            default

A secret is never printed: not here, not in the manifest, not in the error for a value that would not parse.

What it generates

confx.Manifest resolves the same list without building an application:

variables, err := confx.Manifest[store.Config]("store")
STORE_HOST=
STORE_TIMEOUT=30s
STORE_PASSWORD=

Each entry carries its Go type, whether it is required, whether it holds a secret, and its default rendered as text the variable could carry back. It is the same traversal that fills the config, so a variable it lists is exactly a variable the config reads.

Reporting every problem

A Validate that returns on its first problem reports one problem. validate accumulates them, so a config answers in one pass and a deployment is fixed in one rollout:

import "github.com/uchaloop/validate"

func (c Config) Validate() error {
	var errs validate.Errors

	errs.Require(c.Timeout > 0, "timeout must be positive, got %s", c.Timeout)
	errs.Require(len(c.Host) != 0, "host is required")

	return errs.Err()
}

It is a module of its own and depends on nothing outside the standard library, this one included, so a library declaring a config validates it without taking the loader on as a dependency.

Documentation

The tags, the rules and the reasons behind them are in the package documentation: pkg.go.dev/github.com/uchaloop/confmaker/confx.

Acknowledgements

I am grateful to the authors of Uber Fx, and to the authors of env, whose tag vocabulary this library follows and whose implementation it learned from.

License

MIT

Directories

Path Synopsis
Package confx builds a library's typed config from the environment and provides it into an Uber Fx container, so the library consumes a ready config without reading the environment itself.
Package confx builds a library's typed config from the environment and provides it into an Uber Fx container, so the library consumes a ready config without reading the environment itself.

Jump to

Keyboard shortcuts

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