Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ if err := configutil.Set(&cfg); err != nil {
| Option | Description |
|--------------------------------|------------------------------------|
| `WithFilepath("config.env")` | Load values from a `.env` file. |
| `WithSummary(&summary)` | Populate a `LoadSummary` with provenance info for each resolved field. |

### Struct Tags

Expand Down Expand Up @@ -71,6 +72,37 @@ type Config struct {
// Reads SERVER_PORT from sources.
```

### Config Dump / Logging

Use `WithSummary` to inspect what was loaded and from which source. This is invaluable for debugging configuration resolution issues.

```go
type Config struct {
Host string `config:"HOST,default=localhost"`
Port int `config:"PORT"`
}

var cfg Config
var summary configutil.LoadSummary

if err := configutil.Set(&cfg, configutil.WithSummary(&summary)); err != nil {
log.Fatal(err)
}

for _, entry := range summary.Entries {
fmt.Printf("%s=%s (from %s)\n", entry.Key, entry.Value, entry.Source)
}
// Output:
// HOST=localhost (from default)
// PORT=8080 (from env)
```

Each `LoadEntry` in the summary contains:
- `FieldName` — the Go struct field name
- `Key` — the config key that was looked up
- `Value` — the final resolved value (after `${VAR}` substitution)
- `Source` — where the value came from (`"env"`, `"flag"`, a file path, or `"default"`)

## Precedence

Sources are evaluated in order. Later sources overwrite earlier ones.
Expand Down
24 changes: 18 additions & 6 deletions configutil.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// Package configutil populates a struct from environment variables, flags, and .env files.
package configutil

import (
"maps"
)

// Option configures the behaviour of [Set].
type Option func(*settings)

Expand All @@ -16,12 +12,25 @@ func WithFilepath(path string) Option {
}
}

// WithSummary registers out to receive provenance information after Set returns.
// Each config field that receives a value will have a corresponding LoadEntry
// in out.Entries describing the field name, key, resolved value, and source.
// WithSummary(nil) is a no-op.
func WithSummary(out *LoadSummary) Option {
return func(s *settings) {
if out != nil {
s.summary = out
}
}
}

// Set populates config from the registered sources.
// Sources are evaluated in order: files, environment variables, flags.
// Later sources overwrite earlier ones.
func Set(config any, opts ...Option) error {
s := &settings{
source: make(map[string]string),
source: make(map[string]string),
provenance: make(map[string]string),
}

for _, opt := range opts {
Expand All @@ -37,7 +46,10 @@ func Set(config any, opts ...Option) error {
return err
}

maps.Copy(s.source, values)
for k, v := range values {
s.source[k] = v
s.provenance[k] = src.Name()
}
}

return s.populateStruct(config)
Expand Down
Loading
Loading