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>
This commit is contained in:
parent
3c806e6803
commit
c4ebd80669
15 changed files with 941 additions and 0 deletions
150
README.md
150
README.md
|
|
@ -1,2 +1,152 @@
|
|||
# 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`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue