confmaker

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 17 Imported by: 0

README

confmaker

confmaker

Go Reference CI Coverage Release

Explicit, centralized configuration for Go applications. Packages declare what they need; the application registers their configs, loads ENV once and passes typed values to consumers. The core works independently of any framework; confx adapts it to Uber Fx.

[!NOTE] confmaker supports the Twelve-Factor Config approach: declare typed configuration in code and supply deployment-specific values through ENV. Prefixes organize independent variables, not environment profiles. Keep credentials and deployment-specific settings out of code defaults; manifest and exports describe configuration rather than replace ENV.

Install · Quick start · How it works · Rules · Manifest · Diagnostics · Reference

Installation

Requires Go 1.27.0 or later. The command below installs the latest stable release. This README describes the current branch; for a pinned release, use its tagged documentation.

go get github.com/uchaloop/confmaker@latest
import "github.com/uchaloop/confmaker"

The library reads ENV. It does not automatically load .env files, contact secret stores or start services. Set variables through your IDE, shell or deploy system.

Quick start

In a real application, StoreConfig and JobConfig below belong to their respective packages. They need no dependency on confmaker. The application owns the explicit registration list.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/uchaloop/confmaker"
)

type StoreConfig struct {
	Host    string        `env:"HOST,notEmpty" envDescription:"Database address"`
	Timeout time.Duration `env:"TIMEOUT" envDescription:"Database operation 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")
	}
	return nil
}

type JobConfig struct {
	Workers int `env:"WORKERS"`
}

func (c *JobConfig) SetDefaults() { c.Workers = 2 }

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	loader := confmaker.MakeLoader()
	store := loader.Register[StoreConfig]("postgres")
	job := loader.Register[JobConfig]("job")

	if err := loader.Load(); err != nil {
		return err
	}

	storeConfig, err := store.Value()
	if err != nil {
		return err
	}
	jobConfig, err := job.Value()
	if err != nil {
		return err
	}

	// Pass these values to the store and job constructors.
	fmt.Println(storeConfig.Host, storeConfig.Timeout, jobConfig.Workers)
	return nil
}

Run with POSTGRES_HOST set in your IDE, or from a shell:

export POSTGRES_HOST=localhost:5432
go run .

Output: localhost:5432 30s 2. Unset optional fields retain their defaults. For a single configuration, use confmaker.Load[StoreConfig]("postgres").

How it works

flowchart TD
    A["Packages declare Config structs"] --> B["Application registers types and instance names"]
    B --> C["Check declarations and conflicts"]
    C --> D["Take one ENV snapshot"]
    D --> E["SetDefaults → apply ENV → Validate"]
    E --> F{"Any errors?"}
    F -->|Yes| G["Return a combined error; expose no values"]
    F -->|No| H["Pass typed values to consumers"]

Register compiles the schema; errors are reported by Load or Manifest. Load checks all valid registrations, including ones with no consumer. Conflicts between registrations stop loading before defaults run. Unknown variables are checked under registered prefixes; unrelated ENV variables are ignored.

Load runs once. Later calls return the same result. All handles remain unavailable until the whole set loads successfully. A failed configuration prevents values from being handed out for the entire set. Register every config before loading: registrations attempted after Load starts return handles whose Value() reports ErrRegisteredAfterLoad. Before loading completes, existing handles return ErrNotLoaded.

Names and multiple instances

The instance name is explicit and determines the default ENV prefix:

Registration ENV prefix
Register[StoreConfig]("postgres") POSTGRES_
Register[StoreConfig]("read-replica") READ_REPLICA_
Register[StoreConfig]("replica", confmaker.WithPrefix("READ_DB_")) READ_DB_

Register the same type more than once under different names when configuring independent instances. Names contain lowercase letters, digits and _, -, .; they must start and end with a letter or digit. Names, resolved prefixes and full variable names must not collide.

WithPrefix changes ENV names, not the instance name. Config options apply to one registration; ENV options such as WithEnv and AllowUnknown apply to the loader.

Configuration rules

Tag Meaning
env:"HOST" Read <PREFIX>HOST; absence retains the default
env:"HOST,required" ENV must contain the variable; empty text must still be parseable
env:"HOST,notEmpty" ENV must contain non-empty text
env:"-" Ignore the field and everything inside it
envPrefix:"POOL_" Extend the prefix for a nested config held by value
envDescription:"Database address" Description for manifest and exports
envSeparator:";" Separator for a plain slice or map; default ,
envKeyValSeparator:"=" Key/value separator for a plain map; default :
envFormat:"json" Read a slice, array or map from one JSON variable

Defaults belong in Go code, exposed through SetDefaults, not envDefault tags. Only the root config's SetDefaults and Validate methods run; explicitly compose nested defaults or validation there when needed. Validate runs only after all fields of that configuration parse successfully. There is no tag that makes all nested fields required: mark individual fields.

Strings, bools, integers, floats, durations, text-unmarshalable types, scalar pointers, and supported slices/maps can be read in plain syntax. Examples: 30s, host-a,host-b, team:core,env:dev.

For collections, use Headers map[string]string with env:"HEADERS" envFormat:"json", for example APP_HEADERS={"X-Service":"catalog"}. Nested collections and scalar text types are supported; ordinary structs are rejected at every depth. Use envPrefix for nested configuration structs. JSON replaces the whole default, and durations use strings such as "30s". Text types use JSON strings and UnmarshalText / MarshalText, even when they also implement JSON methods. Exporting a text default without MarshalText returns ErrorDefaultRender; loading remains available.

Defaults outside the loader

For packages also used in tests or assembled without ENV, keep defaults in a factory and let SetDefaults delegate to it:

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

func DefaultConfig() Config {
    return Config{Timeout: 30 * time.Second}
}

func (c *Config) SetDefaults() {
    *c = DefaultConfig()
}

func (c Config) Validate() error {
    if len(c.Host) == 0 {
        return fmt.Errorf("host must not be empty")
    }

    if c.Timeout <= 0 {
        return fmt.Errorf("timeout must be positive")
    }

    return nil
}

This example uses fmt and time from the standard library. Outside confmaker, call DefaultConfig(), set application-specific fields and call Validate(). With confmaker, use Load[Config]("postgres") or register the type as usual; loading and manifest generation obtain defaults through SetDefaults. The factory is a package convention, not a function confmaker discovers.

Config{} does not apply defaults. The assignment in SetDefaults replaces the whole config, so call it before applying overrides. Keep defaults deterministic and free of side effects: manifest generation evaluates them on fresh instances. The required and notEmpty tags constrain supplied ENV; Validate checks the resulting config regardless of how it was constructed, when called by the loader or explicitly by its caller.

Structured errors

The original error is the complete result. Structured problems let callers inspect configuration failures without parsing messages:

if err := loader.Load(); err != nil {
	for _, problem := range confmaker.ConfigErrors(err) {
		fmt.Printf("%s: config=%s variable=%s field=%s: %v\n",
			problem.Kind, problem.InstanceName, problem.VariableName,
			problem.FieldPath, problem)
	}
	return err
}

Categories distinguish declarations, conflicts, missing or empty variables, parsing, unknown variables, validation and default rendering. errors.As finds the first *confmaker.ConfigError. ConfigErrors collects structured problems through wrappers and joins; a joined user Validate error remains one validation problem. Treat returned error pointers as read-only.

Ordinary parser errors and user validation errors may include input values; use value-free load reports when logging values is inappropriate. Secret parse values and causes are suppressed.

Original non-secret causes remain available through errors.Is and errors.As, including strconv.ErrSyntax, strconv.ErrRange and *strconv.NumError. Writer and lifecycle errors are separate, so always handle the original error even if ConfigErrors returns no entries.

Load reports

Manifest describes declarations; diagnostics describe one actual load. Enable collection explicitly to inspect sources and results without recording values:

diagnostics := confmaker.MakeDiagnostics()
loader := confmaker.MakeLoader(confmaker.WithDiagnostics(diagnostics))
loader.Register[StoreConfig]("store")

err := loader.Load()
report := diagnostics.Report() // Available even when err != nil.
_ = report                    // Inspect or format it in application code.
if err != nil {
    return err
}

LoadReport contains registration names, types, field paths, sources, statuses and structured LoadProblem categories. It contains no values, defaults, error messages or error causes. The original error returned by Load remains unchanged. Manifest exports may contain non-secret defaults. Keep credentials out of defaults and user-generated validation errors.

Source Meaning
env ENV is present, including empty or invalid input
default ENV is absent and an optional field retains a nonzero default
zero ENV is absent and an optional field retains its zero value
missing Required ENV is absent, even if the field has a default
unknown The field was not processed

An explicit zero in SetDefaults is indistinguishable from an untouched zero. Field success means ENV application succeeded; Validate may still fail the config. Global problems can fail loading even when individual configs succeeded. Problems contains structured library errors without their messages or causes.

All registrations are included, even invalid ones. Fields unavailable because schema compilation failed cannot be listed. Conflicts leave fields unprocessed. Before loading, Report returns LoadNotStarted; during loading it returns LoadInProgress without partial results. Finished reports are independent copies and safe to read concurrently. One Diagnostics receiver belongs to one loader; reusing it for another loader is a declaration error. Repeated Load calls reuse the original result and do not run handlers again. Manifest exports do not collect load diagnostics.

For immediate processing, including fx.New(...).Run(), use a handler instead of a receiver, or combine both options:

confmaker.WithDiagnosticHandler(func(report confmaker.LoadReport) {
    // Send selected metadata to your application's logger or metrics.
})

The handler runs synchronously after completion is published and before the executing Load returns, on success or error. It may read handles and reports or call Load again. Concurrent waiting callers may return before the handler ends. A loading panic propagates, stores LoadPanicked in the receiver, and skips the handler. Problems from completed stages remain available; an interrupted stage may have incomplete results. A handler panic propagates without changing the completed load result. Neither option logs automatically or terminates the process.

Testing

Use WithEnv to replace the process environment with an isolated map. The map is copied when the option is created; WithEnv(nil) means an empty environment. There is no fallback to process ENV or merging of maps. Supply WithEnv once per loader; repeated options are declaration errors.

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

	cfg, err := confmaker.Load[StoreConfig]("postgres", confmaker.WithEnv(map[string]string{
		"POSTGRES_HOST": "localhost:5432",
	}))
	if err != nil {
		t.Fatal(err)
	}
	if cfg.Timeout != 30*time.Second {
		t.Fatalf("unexpected timeout: %s", cfg.Timeout)
	}
}

This test uses StoreConfig from the quick start and imports testing and time. No process-wide ENV changes are needed.

Manifest and configuration documentation

What it describes

A manifest describes what the application declares: config instances, resolved prefixes, ENV names, Go types, required/secret flags, defaults and field descriptions. It is not a snapshot of the application's actual settings.

Use it to inspect the full configuration surface, generate a reference for local setup, publish documentation or supply metadata to external tools. Ordinary application loading does not require it.

Use the same registrations

Register configs once, then choose description or normal loading before creating services. The CLI helper below uses this same registration list and exits the description path before any ENV loading. Manifest generation is optional and does not consume the loader: it can also be used programmatically before or after loading.

For programmatic access after registration:

configs, err := loader.Manifest() // []confmaker.ConfigManifest
if err != nil {
	return err
}
for _, config := range configs {
	fmt.Println(config.InstanceName, config.Prefix)
	for _, variable := range config.Variables {
		fmt.Println(variable.Name, variable.Required, variable.Description)
	}
}

confmaker.Manifest[StoreConfig]("postgres") describes a single type instead. Loader.Manifest also checks conflicts between all registered configurations. The same names and options describe the same ENV variables.

Optional CLI helper

Inside a function returning error (such as the quick start's run), use the following setup instead of its registration block. Import flag and os:

describe := confmaker.MakeDescribeFlag(flag.CommandLine)
flag.Parse()

loader := confmaker.MakeLoader()
loader.Register[StoreConfig]("postgres")
loader.Register[JobConfig]("job")

if describe.Requested() {
    return describe.Write(loader, os.Stdout)
}
return loader.Load() // Or load and pass values to service constructors.

Use -describe=env, -describe=markdown or -describe=json. Omission selects normal startup; an unknown or empty format is a flag parsing error. The helper does not parse flags or exit the process itself: your FlagSet controls error handling. It registers only on the supplied FlagSet, not in init. Use a dedicated FlagSet with ContinueOnError when the caller needs to handle parse errors.

Choose an output
Task API
Inspect metadata in Go loader.Manifest()
Generate an ENV reference loader.WriteEnvExample(writer)
Publish a documentation table loader.WriteManifestMarkdown(writer)
Feed external tools loader.WriteManifestJSON(writer)

All exporters accept io.Writer; the caller opens and closes files. Generate into a buffer first if an existing file must not be truncated on generation failure. Writer failures can still leave partial output.

An .env.example is a reference to keep in version control, not a secrets file or an automatically loaded configuration source. For the quick start's store:

# Configuration: "postgres"
# Database address
# Required; must not be empty.
# POSTGRES_HOST=

# Database operation timeout
POSTGRES_TIMEOUT=30s

Safe non-zero scalar defaults become active assignments. Secrets, complex values and values requiring quoting become commented placeholders. Explicit zero, false and empty-string defaults cannot be distinguished from absent defaults. The template is not a shell script or a guarantee of compatibility with every dotenv parser.

Markdown contains one section per config and a table of variables. JSON uses fixed camelCase keys, two-space indentation and a versioned envelope:

{
  "version": 1,
  "configs": []
}

Config entries contain instanceName, prefix and variables. Variable entries contain name, description, type, required, notEmpty, secret, hasDefault and, for non-secret fields, default. Empty lists are arrays. Consumers should check version. Neither camelCase nor snake_case is mandated by JSON itself; camelCase is this exporter's contract.

Guarantees and boundaries
  • Configs are ordered by instance name; variables retain declaration order.
  • Manifest reads no ENV, calls no Validate and does not make handles ready. It can run before or after Load, including a failed load.
  • Each call evaluates fresh defaults and their marshalers. Keep those methods deterministic and free of side effects; concurrent calls require safe methods.
  • Secret defaults are never rendered. Use a type from secret, not an ordinary string containing credentials. Descriptions are public documentation too.
  • Manifest and all exporters are strict: an invalid declaration, conflict or unrepresentable default fails the whole description before output is written. A commented placeholder does not bypass that check.
  • Manifest does not verify Vault keys, deploy settings or service availability. Such checks belong to external tools consuming its metadata.

Important behavior

Rule Why
Defaults do not satisfy required or notEmpty These tags require the deployment to supply ENV
notEmpty checks text, not the decoded value JSON [] is non-empty text; domain limits belong in Validate
Nested configs are held by value Their structure must not depend on allocation or collection contents
Scalar pointers are supported; pointer-only type cycles are rejected A parser must eventually reach a concrete value
Plain ENV maps reject pointer keys and non-reflexive keys such as NaN Address identity or NaN != NaN would defeat duplicate checks; NaN values remain supported
Map keys and values are not silently trimmed Whitespace must not change configuration unnoticed
Plain collection defaults cannot contain nil pointer chains or ambiguous separators The description must not silently change their meaning
Defaults with unrepresentable text can still load Normal loading does not need to render defaults
Loaded configs are read-only by convention Returned structs may share maps, slices and pointers
Unknown variables are checked only under registered prefixes Unrelated process settings belong to other components; use AllowUnknown for explicit exceptions

Reference

  • GoDoc: complete API and type rules.
  • confx: the Uber Fx adapter.
  • Changelog: changes and migration notes.
  • Contributing: project scope, development checks and contribution guidelines.
  • Security policy: private vulnerability reporting and supported versions.

Acknowledgements

Thanks to the authors of caarlos0/env for the tag conventions and implementation ideas that informed confmaker.

License

MIT

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:

  1. SetDefaults, if the config has it, sets defaults in code.
  2. Each variable that is set replaces its field; an unset variable leaves the default.
  3. 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:

  1. Invalid options and registrations are reported; the other configs still load.
  2. Conflicting registrations - two on one name, prefix or variable - end loading here, before any SetDefaults runs.
  3. One snapshot of the environment is taken.
  4. SetDefaults runs for each config.
  5. Variables under a registered prefix that no field reads are reported.
  6. 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

Examples

Constants

This section is empty.

Variables

View Source
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

func AllowUnknown(prefixes ...string) EnvOption

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

func WithEnv(vars map[string]string) EnvOption

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

func (h *Handle[T]) BelongsTo(loader *Loader) bool

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

func (h *Handle[T]) Name() string

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

func (h *Handle[T]) Value() (T, error)

Value returns the loaded config, or an error:

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

type LoadProblem struct {
	Kind         ErrorKind
	InstanceName string
	VariableName string
	FieldPath    string
}

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.

const (
	LoadNotStarted LoadState = "not_started"
	LoadInProgress LoadState = "in_progress"
	LoadSucceeded  LoadState = "succeeded"
	LoadFailed     LoadState = "failed"
	LoadPanicked   LoadState = "panicked"
)

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

func MakeLoader(opts ...EnvOption) *Loader

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

func (l *Loader) Load() error

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

func (l *Loader) WriteEnvExample(writer io.Writer) error

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

func (l *Loader) WriteManifestJSON(writer io.Writer) error

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

func (l *Loader) WriteManifestMarkdown(writer io.Writer) error

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

Directories

Path Synopsis
confx module

Jump to

Keyboard shortcuts

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