confmaker

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 6, 2026 License: MIT Imports: 10 Imported by: 0

README

confmaker

CI Go Reference

Application-level configuration for Go services. A library declares its config as a plain struct with two tag namespaces - koanf (from the file) and env (from the environment); confmaker loads both and fills the struct, then hands it to the library.

The idea: the service owns the config source, the libraries do not. A library receives a ready-made typed Config and never reads files or the environment itself - the tags it declares are inert strings only the app-level loader reads.

Layering (important)

The masked Secret type lives in the separate zero-dependency module github.com/uchaloop/secret/v2. This keeps configuration types declared by libraries independent of confmaker: a library can expose a secret.Secret field without taking a dependency on the application-level loading stack.

The packages are layered as follows:

Package For Dependencies
github.com/uchaloop/secret/v2 libraries and apps — masked secret values standard library only; separate module
github.com/uchaloop/confmaker/validate apps — error accumulator for Validate() standard library only
github.com/uchaloop/confmaker apps only — TOML loader and helpers secret, koanf, mapstructure
github.com/uchaloop/confmaker/confx apps only — Fx wiring from file and environment to typed config confmaker, env11, fx

An infrastructure library such as a Postgres or Kafka client imports github.com/uchaloop/secret/v2 only when its config contains a secret. It does not need confmaker: the koanf and env struct tags it declares are inert strings. The application imports the root package or confx and is responsible for interpreting those tags, loading values, and validating the resulting config.

Install

go get github.com/uchaloop/confmaker

Usage

Fx wiring: one typed config per named instance (confx)

The recommended path. A library declares its config as a plain struct with two tag namespaces - koanf (from the file) and env (from the environment) - and confx fills it and provides it into the container under a name tag:

// declared by a library (e.g. otherlib); both tags are inert strings, so the
// library depends only on github.com/uchaloop/secret/v2.
type Config struct {
	Addr     string        `koanf:"addr" env:"ADDR"`           // file, env overrides
	Timeout  time.Duration `koanf:"timeout"`                   // file only
	Password secret.Secret `koanf:"-" env:"PASSWORD,notEmpty"` // env only (present + non-empty)
}

The common case is a single instance - [otherlib] in the file, OTHERLIB_ in the environment, no tags:

# config/local.toml
[otherlib]
addr    = "localhost:9000"
timeout = "5s"
fx.New(
	confx.LoadModule("config/local.toml"),
	confx.ProvideDefault[otherlib.Config]("otherlib"), // untagged, -> OTHERLIB_PASSWORD
	otherlib.Module(),                                 // untagged client
)

Add named instances only when a service needs a second one - each is its own top-level section, with its own tag and env prefix:

[otherlib]          # default instance
addr = "localhost:9000"

[analytics]         # a second instance of the same library
addr = "analytics:9000"
fx.New(
	confx.LoadModule("config/local.toml"),
	confx.ProvideDefault[otherlib.Config]("otherlib"),        // untagged, OTHERLIB_*
	confx.Provide[otherlib.Config]("analytics", "analytics"), // tagged, ANALYTICS_*
)
  • ProvideDefault[T](section) decodes [section] into a T, fills its env fields, runs T.Validate() if present, and provides T untagged. The env prefix comes from the section's last segment (otherlib -> OTHERLIB_).
  • Provide[T](section, name) does the same but provides T tagged name:"<name>", with the prefix derived from name (analytics -> ANALYTICS_). Override either with confx.WithEnvPrefix(...).
  • Named instances are separate top-level tables. Nesting one under another (a [otherlib.analytics] sub-table while also reading [otherlib]) makes the child a key of the parent, which strict decoding rejects.
  • Precedence for a field with both tags: file first, env overrides.
  • A file value accidentally mapped to secret.Secret is ignored; secrets can only be populated separately, such as from env. With koanf:"-", the key is excluded entirely and strict decoding rejects it as unknown.
  • LoadModule() with no paths reads config.toml from the current directory.
Convention-based environment directories

For services with a fixed deployment convention, LoadDir reads the required ENVIRONMENT variable and selects configuration files from one directory:

config/
├── common.toml   # optional
├── dev.toml
├── stage.toml
└── prod.toml
fx.New(
	confx.LoadDir("config"),
	confx.ProvideDefault[otherlib.Config]("otherlib"),
	otherlib.Module(),
)

Accepted values are dev, stage, prod, and prd; prd is normalized to prod and selects prod.toml. The environment-specific file is required. common.toml, when present, is loaded first and the environment file overrides it. Field-level environment variables applied by ProvideDefault / Provide remain the final override:

common.toml < prod.toml < field env variables

Without Fx, the equivalent API is:

var cfg AppConfig
if err := confmaker.LoadDir(&cfg, "config"); err != nil {
	return err
}

The directory convention is opt-in. Existing Load and confx.LoadModule calls keep accepting explicit paths and never require ENVIRONMENT.

Configuration without a file

Some configurations do not need a file or LoadModule. Use ProvideNoFile / ProvideNoFileDefault to start with the zero value of the config and fill the fields that declare env tags:

fx.New(
	confx.ProvideNoFileDefault[otherlib.Config]("otherlib"), // untagged, OTHERLIB_*
	confx.ProvideNoFile[otherlib.Config]("analytics"),       // tagged, ANALYTICS_*
	otherlib.Module(),
)

These helpers perform no koanf decode and require no Source. Fields without an env tag remain zero-valued; choosing the tags and validating the completed config are the config author's responsibility. The result is validated and provided tagged or untagged just like Provide / ProvideDefault. No-file and file-backed providers can be used in the same application.

Load open configuration (strict TOML)

Without Fx, load a whole struct directly:

type AppConfig struct {
	Service   ServiceConfig              `koanf:"service"`
	Instances map[string]otherlib.Config `koanf:"otherlib"`
}

var cfg AppConfig
if err := confmaker.Load(&cfg, "config/base.toml", "config/prod.toml"); err != nil {
	return err
}
  • With no paths, Load reads config.toml from the current directory.
  • Later files override earlier ones (base + environment overlay).
  • Decoding is strict: an unknown key (a typo, or a field that does not exist on the struct) fails instead of being silently ignored.
  • Values targeting exported secret.Value fields are ignored even if a koanf tag accidentally maps them; files can never populate secrets.
  • Durations are parsed from strings such as "30m"; weak type coercion is off.
Secrets

The secret.Secret type comes from the separate zero-dependency module github.com/uchaloop/secret/v2. It masks itself when formatted, logged, or serialized; retrieving the real value requires an explicit Reveal() call.

Secrets never come from the config file. Before file decoding, confmaker clears supported exported configuration values implementing secret.Value, regardless of their tags; mapped file values for those types are discarded. Unexported fields and map keys are outside the configuration schema and remain untouched. With confx, a secret is then filled from the environment only when it has an env tag. Without Fx, read a single secret directly:

password, err := confmaker.ResolveSecret("OTHERLIB_PASSWORD")
// error names only the variable, never the value
Named instances without Fx

For a non-Fx service that loads a map of instances, pick them by name:

main, err := confmaker.Required(cfg.Instances, "otherlib", "main") // mandatory
reg := confmaker.MakeRegistry(cfg.Instances)                        // dynamic set
inst, err := reg.Get("otherlib", name)
Validate (accumulate all errors)
func (c AppConfig) Validate() error {
	var e validate.Errors
	e.Require(c.Service.Name != "", "service.name is required")
	// ...
	return e.Err()
}

Testing

go test ./...

Acknowledgements

confmaker builds on knadh/koanf, go-viper/mapstructure, caarlos0/env and uber-go/fx. Thanks to their authors and maintainers.

License

MIT.

Documentation

Overview

Package confmaker is application-level configuration for Go services. A library declares its config as a plain struct with `koanf` (file) and `env` (environment) tags; the application loads the file and fills each library's struct, so a library never reads files or the environment itself.

Two entry points build on this root package. confmaker/confx wires configuration into Uber Fx - it loads the file once and provides each library its filled, validated config by name. Load (below) decodes a whole file into a struct for services that do not use Fx.

Infrastructure libraries need only the dependency-free secret type (github.com/uchaloop/secret/v2); importing this package or confx stays optional, for when a library wants more. The `koanf`/`env` tags a library declares are inert strings the app-level loader reads.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Load

func Load(dst any, paths ...string) error

Load reads the given TOML files in order and decodes the merged result into dst using the `koanf:"..."` struct tags. Later files override earlier ones, so a base + environment-overlay layout works by passing base first. With no paths, it reads config.toml from the current working directory.

Decoding is strict: an unknown key in the file (a typo, or a field that does not exist on dst) fails instead of being silently ignored. Values targeting exported secret.Value fields are ignored so a file can never populate them. Durations are parsed from strings such as "30m"; weak type coercion is disabled, so a mistyped value fails rather than being guessed.

func LoadDir added in v0.3.0

func LoadDir(dst any, dir string) error

LoadDir resolves configuration files from dir using the confmaker directory convention and decodes their merged contents into dst.

ENVIRONMENT is required and accepts dev, stage, prod, or prd (an alias for prod). An optional common.toml is loaded first; the required canonical environment file (dev.toml, stage.toml, or prod.toml) is loaded second and overrides common values.

func Required

func Required[T any](instances map[string]T, kind, name string) (T, error)

Required returns the instance named name from a map of configured instances, or an error naming what is missing. kind labels the resource in the error, for example "postgres" or "kafka consumer". Use it to turn a required-but-absent named connection into a clear startup failure.

func ResolveSecret

func ResolveSecret(envName string) (secret.Secret, error)

ResolveSecret reads a secret value from the named environment variable. It returns an error - naming only the variable, never the value - when the variable is unset or empty, so a missing secret fails the startup loudly without leaking anything into logs.

Types

type Registry

type Registry[T any] struct {
	// contains filtered or unexported fields
}

Registry holds a set of named instances of the same type, for resources whose count is only known at runtime. Prefer confx (Fx-wired named instances) for the fixed, mandatory instances of a service; use a Registry only for genuinely dynamic ones.

func MakeRegistry

func MakeRegistry[T any](items map[string]T) *Registry[T]

MakeRegistry returns a Registry over items. The map is used as-is.

func (*Registry[T]) Get

func (r *Registry[T]) Get(kind, name string) (T, error)

Get returns the instance named name, or an error naming what is missing. kind labels the resource in the error.

func (*Registry[T]) Names

func (r *Registry[T]) Names() []string

Names returns the configured instance names in sorted order.

Directories

Path Synopsis
Package confx wires configuration into Uber Fx.
Package confx wires configuration into Uber Fx.
internal
dirconfig
Package dirconfig resolves the convention-based configuration files shared by confmaker and confmaker/confx.
Package dirconfig resolves the convention-based configuration files shared by confmaker and confmaker/confx.
filedecode
Package filedecode contains decode hooks shared by confmaker's file loaders.
Package filedecode contains decode hooks shared by confmaker's file loaders.
Package validate provides a small error accumulator so a Validate method can report every problem in one pass instead of failing on the first.
Package validate provides a small error accumulator so a Validate method can report every problem in one pass instead of failing on the first.

Jump to

Keyboard shortcuts

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