conf/README.md
juancwu c4ebd80669 Add reflective struct-tag config loader
Implements conf.Load to populate tagged structs from a chain of Sources
(env, .env, YAML/JSON/TOML, custom). Supports default values, slice
separators, nested structs, pointer fields, and a Validator hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:35:51 +00:00

152 lines
3.8 KiB
Markdown

# conf
Tiny, reflective config loader for Go. Define a struct, tag the fields, call
`conf.Load`. Values come from one or more `Source`s — env vars, `.env` files,
YAML/JSON/TOML, or anything you implement yourself.
## Install
```sh
go get git.juancwu.dev/juancwu/conf
```
Requires Go 1.26+.
## Usage
Tag your struct and call `Load`. The first source that returns a value wins;
if no source has it, the `default:` tag is used.
```go
package main
import (
"log"
"strings"
"time"
"git.juancwu.dev/juancwu/conf"
"git.juancwu.dev/juancwu/errx"
)
type Config struct {
BindAddr string `env:"BIND_ADDR" default:":8080"`
DatabaseURL string `env:"DATABASE_URL"`
SessionCookieSecure bool `env:"SESSION_COOKIE_SECURE" default:"true"`
SessionIdleTTL time.Duration `env:"SESSION_IDLE_TTL" default:"24h"`
JWTSecret []byte `env:"JWT_SECRET"`
WorkerConcurrency int `env:"WORKER_CONCURRENCY" default:"4"`
Tags []string `env:"TAGS" sep:","`
}
func (c *Config) Validate() error {
const op = "config.Validate"
if strings.TrimSpace(c.DatabaseURL) == "" {
return errx.New(op, "DATABASE_URL is required")
}
if len(c.JWTSecret) == 0 {
return errx.New(op, "JWT_SECRET is required")
}
return nil
}
func main() {
var cfg Config
if err := conf.Load(&cfg, conf.EnvSource()); err != nil {
log.Fatal(err)
}
_ = cfg
}
```
### Multiple sources
Sources are tried in order — earliest wins. Put highest-precedence first:
```go
dotenv, _ := conf.DotEnvFile(".env")
yamlBase, _ := conf.YAMLFile("config.yaml")
err := conf.Load(&cfg,
conf.EnvSource(), // process env wins
dotenv, // .env overrides yaml
yamlBase, // base defaults
)
```
### `.env` files
```go
src, err := conf.DotEnvFile(".env")
```
Supports `KEY=value`, `export KEY=value`, `#` comments, single/double quotes,
and `\n \t \r \\ \"` escapes inside double quotes.
### YAML / JSON / TOML
File sources are flattened into env-style keys: nested maps join with `_` and
keys are uppercased.
```yaml
# config.yaml
bind_addr: ":9000"
session:
idle_ttl: 24h
cookie:
secure: true
```
becomes `BIND_ADDR`, `SESSION_IDLE_TTL`, `SESSION_COOKIE_SECURE` — matching the
same `env:` tags on your struct. `JSONFile` / `TOMLFile` work the same way.
Each loader has a `*Reader` variant for `io.Reader`.
### Custom sources
```go
type Source interface {
Lookup(key string) (string, bool)
}
```
Implement that to pull from Vault, SSM, a database, or wherever.
## Tags
| Tag | Purpose |
|---------------|----------------------------------------------------------|
| `env:"KEY"` | Key looked up in each `Source`. Required to bind. |
| `env:"-"` | Skip the field. |
| `default:"v"` | Used when no `Source` returns the key. |
| `sep:","` | Slice separator (default `,`). |
Untagged struct fields are recursed into, so you can group related values:
```go
type Config struct {
HTTP HTTP
}
type HTTP struct {
Addr string `env:"HTTP_ADDR" default:":8080"`
}
```
## Supported field types
`string`, `[]byte`, `bool`, all sized `int`/`uint`, `float32`/`float64`,
`time.Duration`, `time.Time` (RFC3339), slices of any scalar above, pointers
to any of the above (left `nil` if unset and no default), and nested structs.
## Validation
If your destination type implements `Validate() error`, it's called after
fields are populated. Return an error to fail the load.
## Errors
All errors flow through [`errx`](https://git.juancwu.dev/juancwu/errx) with op
codes like `conf.Load`, so `errors.Is` / `errors.As` work as usual.
## License
MIT — see `LICENSE`.