Documentation
¶
Overview ¶
Package confmaker centralizes explicit configuration of an application's packages, modules and domains. Each package declares a struct with env tags; the application registers its configs with instance names, loads them together and passes typed values to their consumers. Packages do not need to read ENV themselves.
Import the core library:
import "github.com/uchaloop/confmaker"
A Loader is the composition point for multiple configurations, including multiple instances of the same type:
loader := confmaker.MakeLoader()
primary := loader.Register[store.Config]("postgres")
replica := loader.Register[store.Config]("replica")
if err := loader.Load(); err != nil {
return err
}
primaryConfig, err := primary.Value()
if err != nil {
return err
}
replicaConfig, err := replica.Value()
if err != nil {
return err
}
// Pass primaryConfig and replicaConfig to their consumers.
Load is a convenience for loading one configuration. The separate module github.com/uchaloop/confx adapts the same loading rules to Uber Fx. This package reads ENV only: it does not read dotenv files, contact secret stores or start application services. Manifest generation and exports are optional.
Overview ¶
A config is a struct with env tags and, optionally, two methods:
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")
}
return nil
}
With instance name "store", it reads STORE_HOST, STORE_TIMEOUT and STORE_PASSWORD. The tags are plain strings, so the config type need not import confmaker. This example imports github.com/uchaloop/secret/v2 for its secret field.
Declaring configurations ¶
A field that names a variable in its env tag is read from that variable. A struct field without an env tag is not read itself: its fields are, and those with env tags become variables of the config, under the prefix extended by the field's envPrefix tag, if any. Untagged fields that contain no configuration are ignored; unsupported pointer or collection nesting of config fields is rejected. env:"-" excludes a field and everything nested in it.
type Config struct {
Host string `env:"HOST"`
Pool PoolConfig `envPrefix:"POOL_"`
}
STORE_HOST
STORE_POOL_MAX_CONNS
A nested config must be held by value. This gives the application a fixed set of fields independent of pointer allocation or collection contents. envPrefix is written into names as it stands, with or without a trailing underscore.
A field may be a string, a bool, any sized integer or float, a time.Duration, a type implementing encoding.TextUnmarshaler, a pointer to one of those, or a slice or map of them. A TextUnmarshaler decodes its own text and reports its own errors; for a manifest it also needs encoding.TextMarshaler. complex, uintptr and []byte are refused. A slice splits on envSeparator ("," by default). A map splits entries the same way and each key from its value on envKeyValSeparator (":" by default). The separator tags apply only to a slice or map read in this plain syntax, not to a scalar, a type with its own text form or a JSON field:
STORE_BROKERS=a:9092,b:9092 STORE_LABELS=env:prod,team:core
A duplicate map key is an error, and a key or value padded with whitespace is reported rather than trimmed.
A set variable replaces its field. An empty variable is read like any other text: a string becomes "", a slice or map becomes empty, a TextUnmarshaler receives empty text, a number, bool or duration fails to parse, and a pointer reads its element the same way.
Each field's variable is read under these tag options:
- required: the variable must be set; empty text is allowed if its type can parse it;
- notEmpty: the variable must be set and its text must be non-empty. It checks the text, not the result: JSON [] is non-empty text for an empty slice. Limits on what a value holds belong in Validate.
A default does not satisfy either option: they concern what the deployment supplies.
Pointer-only type cycles are rejected as declaration errors. Pointers to scalar values remain supported, including multiple levels. Untagged nested configs must be structs held by value; hiding them behind pointers or collections at any depth is rejected. Plain ENV maps cannot use pointer keys, whose address identity would bypass duplicate-key checks. Parsed keys must equal themselves: NaN keys, including custom keys containing NaN, are rejected. NaN values remain valid. Ordinary structs inside JSON are rejected.
Declarations are checked when a config is registered, before any default or variable is read, and a mistake is reported by Loader.Load, Loader.Manifest or Manifest: envDefault (defaults belong in SetDefaults), an unknown option or options without a name, two fields on one variable, envPrefix on anything but a struct nested by value, a secret in a slice, array or map, a config nested through a pointer or collection, an unreadable type, an empty separator or one on a field that is not split, and a variable name no environment can hold (with = or NUL). Secrets are recognised behind any number of pointers.
Names and prefixes ¶
Every config has an instance name. It gives the default prefix - "read-replica" reads READ_REPLICA_* - and labels the config in errors. It comes from the required first argument of Loader.Register, Load or Manifest:
loader.Register[Config]("store") // STORE_*
loader.Register[Config]("replica") // REPLICA_*
loader.Register[Config]("main", confmaker.WithPrefix("MAIN_DB_")) // MAIN_DB_*
Names hold lowercase letters, digits and _ - ., and do not start or end with a separator. An empty name is invalid, even with WithPrefix. Two registrations may not share a name, a prefix, or a variable.
Options come in two kinds: a ConfigOption configures one config, an EnvOption configures a whole load. Passing one where the other is expected does not compile. Load takes both.
Defaults and validation ¶
Loading a config runs these steps:
- SetDefaults, if the config has it, sets defaults in code.
- Each variable that is set replaces its field; an unset variable leaves the default.
- Validate, if the config has it, checks the result. It runs only when every variable of the config parsed.
SetDefaults must be deterministic and free of side effects: loading calls it once per config, and each manifest or export call evaluates it again on a fresh instance. Only the top-level config's methods are called.
For use outside the loader, a package can expose a defaults factory and delegate SetDefaults to it:
func DefaultConfig() Config {
return Config{Timeout: 30 * time.Second}
}
func (c *Config) SetDefaults() {
*c = DefaultConfig()
}
Tests and manually assembled applications can call DefaultConfig, apply their own overrides and call Validate explicitly. confmaker discovers only SetDefaults, not the factory. Config{} does not apply defaults. This SetDefaults implementation replaces the entire value, so it must run before overrides. Keep invariants that must hold outside ENV loading in Validate: required and notEmpty check the supplied ENV, not a manually constructed config.
JSON collections ¶
envFormat:"json" reads a slice, array or map from one variable. A pointer to one of these collections is also supported. Nested collections are allowed:
type Config struct {
Headers map[string]string `env:"HEADERS" envFormat:"json"`
Groups map[string][]string `env:"GROUPS" envFormat:"json"`
}
APP_HEADERS={"X-Service":"catalog"}
APP_GROUPS={"read":["primary","replica"]}
A supplied value replaces the entire default. Duplicate keys and empty input are errors. null clears pointers, maps and slices; it is invalid for other values. Durations use strings such as "30s". Map keys must be strings, integers or supported text types. Defaults render with deterministic map ordering.
Ordinary structs are rejected at every depth, including collection elements. Declare nested configuration by value with envPrefix instead. Types implementing encoding.TextUnmarshaler may represent scalar text values, including time.Time; they are read from JSON strings using UnmarshalText. Rendering their defaults requires encoding.TextMarshaler; otherwise manifest returns ErrorDefaultRender. Text methods take precedence over JSON methods in both directions. Custom text methods must agree on a representation that can be read back. A JSON-only custom decoder does not exempt a struct from this restriction.
Interfaces, byte collections without a custom text form, unsupported kinds and secret types are rejected. Secrets hidden inside custom text types are rejected too. envFormat cannot be combined with separator tags. Errors identify the ENV variable and location in the JSON value, preserving the underlying JSON error.
Loading and lifecycle ¶
Loader.Load runs in this order:
- Invalid options and registrations are reported; the other configs still load.
- Conflicting registrations - two on one name, prefix or variable - end loading here, before any SetDefaults runs.
- One snapshot of the environment is taken.
- SetDefaults runs for each config.
- Variables under a registered prefix that no field reads are reported.
- The environment is applied and Validate runs, for each config.
Problems are joined into one error. Config registrations are processed in a deterministic order rather than the order of Register calls; unknown variables are sorted by name. Errors from different stages retain their stage order:
unknown configuration variable "STORE_HSOT" (did you mean "STORE_HOST"?) config "store": required variable "STORE_HOST" is not set
Values are handed out only when the whole set loaded; see Handle.Value. Load runs once. A Loader is safe for concurrent use and holds no lock while SetDefaults, Validate or unmarshalers run; those may call Register on that Loader or Value on its handles, but must not call its Load.
Environment and testing ¶
Load reads one snapshot of the environment, shared by field parsing and the check for unknown variables. By default it is the process environment; WithEnv replaces it with a map, which suits tests:
cfg, err := confmaker.Load[Config]("store", confmaker.WithEnv(map[string]string{
"STORE_HOST": "localhost",
}))
confmaker reads variables only; reading a file into such a map is left to the caller. Passing nil to WithEnv means an empty environment, not a fallback to the process. WithEnv copies the map when the option is created. AllowUnknown exempts prefixes from the check for unknown variables, for an environment shared with other programs.
Structured errors ¶
ConfigError exposes an ErrorKind, InstanceName, VariableName and FieldPath. Context is empty when unavailable. errors.As finds the first structured problem; ConfigErrors walks wrappers and joins to collect all problems in tree order. Each ConfigError counts as one problem, including a Validate error whose cause is itself a join. Its original cause remains reachable through errors.Is and errors.As. Returned pointers should be treated as read-only.
Standard scalar parsers retain their original causes without changing the human-readable message. Bool, integer and float errors expose strconv.NumError and its syntax or range cause. Duration errors retain the time parser's cause.
Secret parse causes are discarded rather than exposed through Unwrap. Context fields do not contain ENV values, but ordinary error messages and user Validate errors may contain them. Writer and lifecycle errors remain separate; always handle the original error even when ConfigErrors returns no problems.
Load reports ¶
WithDiagnostics collects value-free LoadReport snapshots in a Diagnostics receiver. WithDiagnosticHandler handles a completed report synchronously before Load returns, on success or error. Neither option reads ENV again or repeats defaults, parsing or validation. Reports contain sources, statuses and error categories, never values, messages or causes. Collection is optional and independent of manifest generation. A receiver belongs to one loader; Report is safe for concurrent use and returns independent copies. Loading panics remain panics and skip the handler; the receiver records LoadPanicked. See WithDiagnosticHandler for completion and callback ordering.
Manifest and secrets ¶
Loader.Manifest describes a snapshot of all registrations as []ConfigManifest, ordered by instance name, with variables in declaration order. It checks invalid registrations and conflicts before invoking config methods. Any declaration, conflict or rendering error returns a nil result. Load options, including WithEnv, are ignored.
configs, err := loader.Manifest()
It can run before, during or after Load, without reading ENV, calling Validate or changing loader state. Each call evaluates defaults on fresh instances and never reads loaded values. Registrations completed after the snapshot are included on the next call. If called concurrently with Load or another Manifest, user-supplied defaults and marshalers must support concurrent calls. No loader lock is held while those methods run.
Loader.WriteEnvExample writes a reference .env.example to an io.Writer using that same manifest. The envDescription tag supplies Variable.Description and comments in the template. Safe non-zero scalar defaults become assignments; secrets, complex values and defaults requiring quoting become commented placeholders. Zero defaults are indistinguishable from absent defaults. Names unsuitable for dotenv assignments are preserved in quoted comments. This does not impose additional restrictions on names accepted by Load. The caller owns file handling; no dotenv file is read automatically. Manifest errors write nothing; writer errors may leave partial output.
MakeDescribeFlag optionally registers -describe on a caller-supplied flag.FlagSet. After parsing, check DescribeFlag.Requested and call DescribeFlag.Write to export env, markdown or json. Unknown formats fail flag parsing. The helper does not parse arguments, load configs or exit the process.
Loader.WriteManifestJSON exports a versioned JSON document with fixed camelCase keys, preserving all variable metadata except secret defaults. Loader.WriteManifestMarkdown exports sections and tables for documentation, escaping text and distinguishing required from required non-empty variables. Both use the same Manifest lifecycle and ordering, finish preparation before writing and leave file handling to the caller. Writer failures can leave partial output. JSON's hasDefault retains the non-zero semantics described below.
Manifest describes declarations and defaults without reading ENV. Diagnostics describes a load's sources and results without recording values.
Manifest and all exporters are strict: if any default cannot be rendered, the whole description fails. This includes nil pointers anywhere in a plain collection element or a default that its separators cannot represent. A commented .env.example placeholder does not bypass that check. Ordinary loading does not require defaults to be rendered. Variable.HasDefault reports a non-zero default and cannot tell an explicit false, 0 or "" from no default. Manifest values are ENV text; a generator escapes them for shell or YAML itself.
Secret defaults (github.com/uchaloop/secret/v2) are excluded from manifest exports and secret parse values are omitted from library errors. Ordinary defaults are included in manifest exports; user validation errors may contain values. Keep credentials out of non-secret defaults and user-generated error messages.
Index ¶
- Variables
- func Load[T any](name string, opts ...LoadOption) (T, error)
- type ConfigError
- type ConfigManifest
- type ConfigOption
- type ConfigReport
- type ConfigStatus
- type DescribeFlag
- type Diagnostics
- type EnvOption
- type ErrorKind
- type Handle
- type LoadOption
- type LoadProblem
- type LoadReport
- type LoadState
- type Loader
- func (l *Loader) Load() error
- func (l *Loader) Manifest() ([]ConfigManifest, error)
- func (l *Loader) Register[T any](name string, opts ...ConfigOption) *Handle[T]
- func (l *Loader) WriteEnvExample(writer io.Writer) error
- func (l *Loader) WriteManifestJSON(writer io.Writer) error
- func (l *Loader) WriteManifestMarkdown(writer io.Writer) error
- type ValueSource
- type Variable
- type VariableReport
- type VariableStatus
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotLoaded is returned by [Handle.Value] until [Loader.Load] has finished. ErrNotLoaded = errors.New("configuration is not loaded; call Load first") // ErrRegisteredAfterLoad is returned by [Handle.Value] for a config added // with [Loader.Register] once Load had started. ErrRegisteredAfterLoad = errors.New("config registered after Load started; register every config before loading") )
Functions ¶
func Load ¶
func Load[T any](name string, opts ...LoadOption) (T, error)
Load loads one config of type T, as a Loader with a single registration would. The required name determines the default ENV prefix. Options include ConfigOption (WithPrefix) and EnvOption (WithEnv, AllowUnknown, WithDiagnostics, WithDiagnosticHandler). Unknown variables are looked for under T's own prefix only. On error it returns the zero T.
cfg, err := confmaker.Load[store.Config]("store")
cfg, err := confmaker.Load[store.Config]("replica", confmaker.WithEnv(vars))
Example ¶
Load reads one config. WithEnv stands in for the process environment here, so the example runs the same everywhere; an application calls Load without it.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker"
)
// 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 (c StoreConfig) Validate() error {
if c.Timeout <= 0 {
return fmt.Errorf("timeout must be positive, got %s", c.Timeout)
}
return nil
}
func main() {
cfg, err := confmaker.Load[StoreConfig]("store", confmaker.WithEnv(map[string]string{
"STORE_HOST": "db:5432",
}))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(cfg.Host, cfg.Timeout)
}
Output: db:5432 30s
Example (Errors) ¶
One error reports every problem, with a suggestion for a misspelled variable. Validate would refuse the zero timeout, but it does not run while a variable of the config is missing: its report comes once the host is fixed.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker"
)
// 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 (c StoreConfig) Validate() error {
if c.Timeout <= 0 {
return fmt.Errorf("timeout must be positive, got %s", c.Timeout)
}
return nil
}
func main() {
_, err := confmaker.Load[StoreConfig]("store", confmaker.WithEnv(map[string]string{
"STORE_HSOT": "db:5432",
"STORE_TIMEOUT": "0s",
}))
fmt.Println(err)
}
Output: unknown configuration variable "STORE_HSOT" (did you mean "STORE_HOST"?) config "store": required variable "STORE_HOST" is not set
Types ¶
type ConfigError ¶ added in v0.8.0
type ConfigError struct {
Kind ErrorKind
InstanceName string
VariableName string
FieldPath string
// contains filtered or unexported fields
}
ConfigError describes one configuration problem. Context fields are empty when unavailable; conflicts involving multiple instances have no single owner. No field holds an ENV value. Error messages and causes for ordinary fields and user validation may contain values; secret parse causes are discarded. Use errors.As to find the first problem or ConfigErrors to collect all of them.
func ConfigErrors ¶ added in v0.8.0
func ConfigErrors(err error) []*ConfigError
ConfigErrors returns all structured configuration problems in err, including wrapped and joined errors. It returns nil if none are present. Validation errors remain one problem even when Validate returns joined errors. Results preserve error order and repeated occurrences. Treat the returned pointers as read-only; they refer to the original errors.
Example ¶
_, err := Load[struct {
URL string `env:"URL,notEmpty"`
}]("api", WithEnv(map[string]string{}))
for _, problem := range ConfigErrors(err) {
fmt.Println(problem.Kind, problem.InstanceName, problem.VariableName, problem.FieldPath)
}
Output: required api API_URL URL
func (*ConfigError) Error ¶ added in v0.8.0
func (e *ConfigError) Error() string
Error returns the problem's message. The surrounding load error adds the instance label when applicable.
func (*ConfigError) Unwrap ¶ added in v0.8.0
func (e *ConfigError) Unwrap() error
Unwrap preserves the original cause, except for secret parse errors, whose unsafe cause is replaced with a value-free message.
type ConfigManifest ¶ added in v0.8.0
type ConfigManifest struct {
// InstanceName is the name passed to Loader.Register.
InstanceName string
// Prefix is the resolved ENV prefix, including any WithPrefix override.
Prefix string
// Variables lists fields in declaration order. Secret defaults are omitted.
Variables []Variable
}
ConfigManifest describes one registered configuration and its environment variables. It contains defaults, not values read from the environment.
type ConfigOption ¶ added in v0.6.2
type ConfigOption interface {
LoadOption
// contains filtered or unexported methods
}
ConfigOption configures one config with WithPrefix. Loader.Register, Manifest and Load take it.
func WithPrefix ¶ added in v0.6.2
func WithPrefix(prefix string) ConfigOption
WithPrefix overrides the environment prefix without changing the instance name. It must be non-empty, contain only upper-case letters, digits or underscores, and end with an underscore, for example "REPLICA_POSTGRES_".
Example ¶
The instance name stays primary while WithPrefix selects DATABASE_* variables.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker"
)
// 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 (c StoreConfig) Validate() error {
if c.Timeout <= 0 {
return fmt.Errorf("timeout must be positive, got %s", c.Timeout)
}
return nil
}
func main() {
cfg, err := confmaker.Load[StoreConfig]("primary",
confmaker.WithPrefix("DATABASE_"),
confmaker.WithEnv(map[string]string{"DATABASE_HOST": "db:5432"}),
)
if err != nil {
fmt.Println(err)
return
}
variables, err := confmaker.Manifest[StoreConfig]("primary", confmaker.WithPrefix("DATABASE_"))
if err != nil {
fmt.Println(err)
return
}
fmt.Println(cfg.Host, variables[0].Name)
}
Output: db:5432 DATABASE_HOST
type ConfigReport ¶ added in v0.9.0
type ConfigReport struct {
InstanceName string
Type string
Prefix string
Status ConfigStatus
Variables []VariableReport
}
ConfigReport describes one registration, including its explicit name and Go type.
type ConfigStatus ¶ added in v0.9.0
type ConfigStatus string
ConfigStatus describes processing of a config, independently of other configs.
const ( ConfigNotProcessed ConfigStatus = "not_processed" ConfigSucceeded ConfigStatus = "succeeded" ConfigFailed ConfigStatus = "failed" )
type DescribeFlag ¶ added in v0.9.0
type DescribeFlag struct {
// contains filtered or unexported fields
}
DescribeFlag selects an optional manifest export. Create it with MakeDescribeFlag, parse its FlagSet, then check Requested before Write. It does not parse arguments, load ENV or terminate the process. Like flag.FlagSet, it is not intended for concurrent mutation.
func MakeDescribeFlag ¶ added in v0.9.0
func MakeDescribeFlag(flags *flag.FlagSet) *DescribeFlag
MakeDescribeFlag registers -describe in flags. Accepted formats are env, markdown and json; omission means normal application startup. Invalid formats are rejected by FlagSet.Parse. As with other flag registration functions, duplicate flag names panic. flags must be non-nil.
func (*DescribeFlag) Requested ¶ added in v0.9.0
func (d *DescribeFlag) Requested() bool
Requested reports whether a valid description format was selected. Check FlagSet.Parse's error before using the result.
func (*DescribeFlag) Write ¶ added in v0.9.0
func (d *DescribeFlag) Write(loader *Loader, writer io.Writer) error
Write exports the selected format through loader to writer. It does not load configurations. Calling Write without selecting a format returns an error. Export and writer errors are passed through unchanged. loader must be non-nil.
type Diagnostics ¶ added in v0.9.0
type Diagnostics struct {
// contains filtered or unexported fields
}
Diagnostics stores the report of one Loader. Its zero value is ready for use. It is safe for concurrent use and must not be copied after first use.
func MakeDiagnostics ¶ added in v0.9.0
func MakeDiagnostics() *Diagnostics
MakeDiagnostics creates a report receiver for one loader.
func (*Diagnostics) Report ¶ added in v0.9.0
func (d *Diagnostics) Report() LoadReport
Report returns an independent snapshot. Before loading it reports LoadNotStarted; during loading it reports LoadInProgress without partial results. Reading a report never loads configs or waits for user callbacks.
type EnvOption ¶ added in v0.6.2
type EnvOption interface {
LoadOption
// contains filtered or unexported methods
}
EnvOption configures a whole load: WithEnv, AllowUnknown, WithDiagnostics and WithDiagnosticHandler. MakeLoader and Load take it.
func AllowUnknown ¶ added in v0.6.2
AllowUnknown exempts variables starting with the given prefixes from the check for unknown variables, for an environment shared with other programs. The prefixes are copied. An empty prefix is ignored: it would match every variable and turn the check off.
func WithDiagnosticHandler ¶ added in v0.9.0
func WithDiagnosticHandler(handler func(LoadReport)) EnvOption
WithDiagnosticHandler enables collection and calls handler synchronously once after load completion is published, on success or error, before Load returns. The handler receives an independent report and may read handles or call Load. It is not called when loading panics. Handler panics propagate without changing the completed load result. A nil or repeated handler is a declaration error. Concurrent Load callers may return before the handler finishes. No logging, process termination or error recovery is performed by this option.
func WithDiagnostics ¶ added in v0.9.0
func WithDiagnostics(diagnostics *Diagnostics) EnvOption
WithDiagnostics enables collection into diagnostics. A nil receiver, repeated option or receiver already owned by another loader is a declaration error.
func WithEnv ¶ added in v0.6.2
WithEnv makes a load read vars instead of the process environment.
- vars is the complete environment: nothing is added from the process.
- nil is an empty environment, not the process environment.
- The map is copied when WithEnv is called; later changes to it are not seen.
- WithEnv may be given once per load; a second one is reported by Loader.Load.
Without WithEnv, a load takes a snapshot of the process environment when it starts.
type ErrorKind ¶ added in v0.8.0
type ErrorKind string
ErrorKind identifies the stage or condition that produced a ConfigError. The zero value does not identify a category.
const ( // ErrorDeclaration identifies invalid config types, tags, names or options. ErrorDeclaration ErrorKind = "declaration" // ErrorConflict identifies competing names, prefixes or variables. ErrorConflict ErrorKind = "conflict" // ErrorRequired identifies an absent required variable. ErrorRequired ErrorKind = "required" // ErrorEmpty identifies an empty variable declared notEmpty. ErrorEmpty ErrorKind = "empty" // ErrorParse identifies a value that could not be parsed. ErrorParse ErrorKind = "parse" // ErrorUnknownVariable identifies an undeclared variable under a config prefix. ErrorUnknownVariable ErrorKind = "unknown_variable" // ErrorValidation identifies an error returned by Validate. ErrorValidation ErrorKind = "validation" // ErrorDefaultRender identifies a default that could not be rendered. ErrorDefaultRender ErrorKind = "default_render" )
type Handle ¶ added in v0.6.2
type Handle[T any] struct { // contains filtered or unexported fields }
Handle is a config registered with Loader.Register, read with Handle.Value. The zero Handle belongs to no loader, and its Value returns an error.
func (*Handle[T]) BelongsTo ¶ added in v0.9.0
BelongsTo reports whether this handle was registered with loader. Nil, zero and rejected late handles belong to no loader. This does not load or validate.
func (*Handle[T]) Name ¶ added in v0.9.0
Name returns the explicit registration name, without reading ENV or loading. A nil or zero handle has no name.
func (*Handle[T]) Value ¶ added in v0.6.2
Value returns the loaded config, or an error:
- ErrNotLoaded until Loader.Load has finished;
- the error Load returned, when any config of the set failed - even if this config loaded on its own;
- ErrRegisteredAfterLoad for a config added once Load had started.
The struct is returned by value, but maps, slices and pointers inside it are shared by every caller. Treat a loaded config as read-only.
type LoadOption ¶ added in v0.6.2
type LoadOption interface {
// contains filtered or unexported methods
}
LoadOption is an option Load takes: every ConfigOption and every EnvOption.
type LoadProblem ¶ added in v0.9.0
LoadProblem contains only the structural context of a library error. Context is empty when the problem cannot be attributed to one config or field.
type LoadReport ¶ added in v0.9.0
type LoadReport struct {
State LoadState
Configs []ConfigReport
Problems []LoadProblem
}
LoadReport is a value-free snapshot. Problems never contain error messages or causes. Configs include invalid registrations, whose fields may be unavailable. Successful fields do not imply successful config validation or loading. After a panic, completed stages retain their problems; the interrupted stage may be incomplete.
type LoadState ¶ added in v0.9.0
type LoadState string
LoadState describes the progress of one load.
type Loader ¶ added in v0.6.2
type Loader struct {
// contains filtered or unexported fields
}
Loader loads a set of configs together: configs are registered with Loader.Register, loaded with Loader.Load, and read with Handle.Value.
A Loader is safe for concurrent use and holds no lock while user code runs: SetDefaults, Validate and unmarshalers may call Register on that Loader or Value on its handles. They must not call Load recursively: it would wait for the load already in progress.
Example ¶
A Loader loads several configs, here two instances of one type, and reports their problems together.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker"
)
// 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 (c StoreConfig) Validate() error {
if c.Timeout <= 0 {
return fmt.Errorf("timeout must be positive, got %s", c.Timeout)
}
return nil
}
func main() {
loader := confmaker.MakeLoader(confmaker.WithEnv(map[string]string{
"STORE_HOST": "db:5432",
"REPLICA_HOST": "replica:5432",
}))
primary := loader.Register[StoreConfig]("store") // STORE_*
replica := loader.Register[StoreConfig]("replica") // REPLICA_*
if err := loader.Load(); err != nil {
fmt.Println(err)
return
}
primaryCfg, _ := primary.Value()
replicaCfg, _ := replica.Value()
fmt.Println(primaryCfg.Host, replicaCfg.Host)
}
Output: db:5432 replica:5432
func MakeLoader ¶ added in v0.6.2
MakeLoader returns an empty Loader. opts configure the whole load: WithEnv, AllowUnknown, WithDiagnostics and WithDiagnosticHandler. A nil option is reported by Loader.Load.
func (*Loader) Load ¶ added in v0.6.2
Load loads every registered config and returns every problem as one error: invalid options and registrations, conflicts between registrations, unknown variables, variables that did not parse, and the Validate errors of every config whose variables parsed. The steps are listed in the package documentation.
Load runs once. Calling it again returns the same error, or nil, without reading the environment again; a call made while another goroutine loads waits for that result. If user code panics during the load, the panic propagates from the Load that ran it, and later calls return an error.
func (*Loader) Manifest ¶ added in v0.8.0
func (l *Loader) Manifest() ([]ConfigManifest, error)
Manifest describes a snapshot of all registrations, ordered by instance name. Each config's variables retain declaration order. Invalid registrations and conflicts are reported before any config methods run. Rendering errors are collected across configs; any error returns a nil result, never a partial list.
Manifest does not read ENV, call Validate or change loader state. Loading options and their errors are ignored here. Each call runs SetDefaults and default marshalers on fresh instances; it never inspects or changes loaded values or makes a Handle ready.
Manifest can run before, during or after Load, including after a failed load. Concurrent registrations completed after the snapshot belong to a later call. No loader lock is held while config methods run. These methods must be safe for concurrent calls if Manifest or Load are called concurrently. As with the package-level Manifest, a panic from user code propagates to the caller.
Example ¶
Manifest describes the registered set without reading required ENV values.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker"
)
// 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 (c StoreConfig) Validate() error {
if c.Timeout <= 0 {
return fmt.Errorf("timeout must be positive, got %s", c.Timeout)
}
return nil
}
func main() {
loader := confmaker.MakeLoader()
loader.Register[StoreConfig]("store")
loader.Register[StoreConfig]("replica", confmaker.WithPrefix("REPLICA_STORE_"))
configs, err := loader.Manifest()
if err != nil {
fmt.Println(err)
return
}
for _, config := range configs {
fmt.Println(config.InstanceName, config.Prefix)
for _, variable := range config.Variables {
fmt.Printf("%s=%s\n", variable.Name, variable.Default)
}
}
}
Output: replica REPLICA_STORE_ REPLICA_STORE_HOST= REPLICA_STORE_TIMEOUT=30s store STORE_ STORE_HOST= STORE_TIMEOUT=30s
func (*Loader) Register ¶ added in v0.8.0
func (l *Loader) Register[T any](name string, opts ...ConfigOption) *Handle[T]
Register registers a struct config of type T and returns its Handle. The required name labels errors and determines the default ENV prefix: "read-replica" reads READ_REPLICA_*. WithPrefix overrides only the prefix. Names must be non-empty, contain only lowercase letters, digits, _ - ., and start and end with a letter or digit.
An invalid registration, such as an empty name or bad tag, is reported by Loader.Load or Loader.Manifest. Once Load has started, Register registers nothing and calls no method of T; the handle's Value returns ErrRegisteredAfterLoad.
func (*Loader) WriteEnvExample ¶ added in v0.8.0
WriteEnvExample writes a reference .env.example for the same snapshot as Loader.Manifest. It does not read ENV, load configs or open files. Descriptions become comments. Only non-secret, non-zero scalar defaults made of ASCII letters, digits and _./:@%+,- are emitted as active assignments. Other fields become commented placeholders; complex defaults are not printed. Required fields are marked even when they have a default. Zero defaults cannot be distinguished from absent defaults and also become placeholders.
Names outside [A-Za-z_][A-Za-z0-9_]* are quoted in comments, without changing the names accepted by Load. The output is a reference, not a shell script or a promise of compatibility with every dotenv parser. Manifest errors leave the writer untouched. Writer errors are returned and may leave partial output. Defaults and marshalers have the same lifecycle as Manifest. Secret values are never rendered.
Example ¶
loader := MakeLoader()
loader.Register[struct {
URL string `env:"URL,notEmpty" envDescription:"Service endpoint"`
}]("api")
var output bytes.Buffer
if err := loader.WriteEnvExample(&output); err != nil {
panic(err)
}
fmt.Print(output.String())
Output: # Configuration: "api" # Service endpoint # Required; must not be empty. # API_URL=
func (*Loader) WriteManifestJSON ¶ added in v0.8.0
WriteManifestJSON writes a version 1 manifest with fixed camelCase keys, two-space indentation and a trailing newline. Empty lists are JSON arrays. All variable metadata is included except secret defaults, whose default key is omitted. hasDefault retains Manifest's non-zero-value semantics.
It uses Loader.Manifest's snapshot and lifecycle: no ENV is read or config loaded. Manifest and encoding errors leave writer untouched. Writer errors are returned and may leave partial output. The caller owns file handling.
Example ¶
loader := MakeLoader()
var output bytes.Buffer
if err := loader.WriteManifestJSON(&output); err != nil {
panic(err)
}
fmt.Print(output.String())
Output: { "version": 1, "configs": [] }
func (*Loader) WriteManifestMarkdown ¶ added in v0.8.0
WriteManifestMarkdown writes a section per config with its prefix and a GitHub-flavored Markdown table of variables in manifest order. Columns show ENV name, Go type, requirement, secret status, default and description. Missing or secret defaults are shown as an em dash. Text is escaped for table cells; description and default line breaks become HTML br elements.
It uses Loader.Manifest's snapshot and lifecycle without reading ENV or loading configs. Manifest errors leave writer untouched. Writer errors are returned and may leave partial output. The caller owns file handling.
Example ¶
loader := MakeLoader()
loader.Register[struct {
URL string `env:"URL,notEmpty" envDescription:"Service endpoint"`
}]("api")
var output bytes.Buffer
if err := loader.WriteManifestMarkdown(&output); err != nil {
panic(err)
}
fmt.Print(output.String())
Output: # Configuration manifest ## api Prefix: `API_` | ENV | Type | Requirement | Secret | Default | Description | | --- | --- | --- | --- | --- | --- | | `API_URL` | `string` | Required, non-empty | No | — | Service endpoint |
type ValueSource ¶ added in v0.9.0
type ValueSource string
ValueSource identifies the input used or required by a field. A zero value explicitly assigned in SetDefaults cannot be distinguished from an untouched zero.
const ( SourceUnknown ValueSource = "unknown" SourceEnv ValueSource = "env" SourceDefault ValueSource = "default" SourceZero ValueSource = "zero" SourceMissing ValueSource = "missing" )
type Variable ¶ added in v0.6.2
type Variable struct {
// Name is the full variable name, prefix included.
Name string
// Description is the optional envDescription tag on the field.
Description string
// Type is the Go type of the field it fills.
Type string
// Required reports whether the variable must be set: the field is declared
// required or notEmpty.
Required bool
// NotEmpty reports whether the variable must also hold a non-empty value: the
// field is declared notEmpty. Without it, required permits empty text,
// provided the field type can parse it.
NotEmpty bool
// Secret reports whether the field holds a secret value.
Secret bool
// Default is the value the field holds after SetDefaults, rendered as the
// text a variable would carry. It is empty for a secret.
Default string
// HasDefault reports whether the field is non-zero before the environment is
// applied. A default of false, 0 or "" is indistinguishable from no default
// and is reported as false. Secret fields always report false.
HasDefault bool
// contains filtered or unexported fields
}
Variable describes one environment variable a config reads. The set of them comes from the same compiled schema used to load it, so a variable listed here is exactly a variable the config reads.
func Manifest ¶ added in v0.6.2
func Manifest[T any](name string, opts ...ConfigOption) ([]Variable, error)
Manifest returns the variables a config of type T reads, in declaration order, for generating a .env.example, a config map or a documentation table. The required name and options match Loader.Register, so the same arguments describe the same environment variables.
Manifest does not read the environment. It does call T's SetDefaults, and renders each default with MarshalText or the field's plain syntax; a default that cannot be rendered is an error. The result is metadata: Variable.Default is ENV text, not escaped for a shell or YAML.
A declaration Loader.Load would refuse is refused here too, before SetDefaults runs.
Example ¶
Manifest lists the variables and their defaults without reading the environment. The output is ENV names and values, not an escaped shell script.
package main
import (
"fmt"
"time"
"github.com/uchaloop/confmaker"
)
// 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 (c StoreConfig) Validate() error {
if c.Timeout <= 0 {
return fmt.Errorf("timeout must be positive, got %s", c.Timeout)
}
return nil
}
func main() {
variables, err := confmaker.Manifest[StoreConfig]("store")
if err != nil {
fmt.Println(err)
return
}
for _, v := range variables {
fmt.Printf("%s=%s\n", v.Name, v.Default)
}
}
Output: STORE_HOST= STORE_TIMEOUT=30s
type VariableReport ¶ added in v0.9.0
type VariableReport struct {
Name string
FieldPath string
Source ValueSource
Status VariableStatus
}
VariableReport describes one compiled field without its value or default.
type VariableStatus ¶ added in v0.9.0
type VariableStatus string
VariableStatus describes ENV application to a field, not config validation.
const ( VariableNotProcessed VariableStatus = "not_processed" VariableSucceeded VariableStatus = "succeeded" VariableFailed VariableStatus = "failed" )
