confmaker

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: MIT Imports: 8 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. 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 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 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.
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 secret placed in the file (koanf:"-") is rejected as an unknown key.
  • LoadModule() with no paths reads config.toml from the current directory.
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.
  • 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. It masks itself when formatted, logged, or serialized; retrieving the real value requires an explicit Reveal() call.

Secrets never live in the config file. With confx, an env-tagged field is filled from the environment. 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); 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. Durations are parsed from strings such as "30m"; weak type coercion is disabled, so a mistyped value fails rather than being guessed.

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