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
9 changes: 4 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ Loggers: `logger.NewFmtLogger`, `logger.NewSlogLogger` (Go 1.21+), and the adapt
| Method | Description |
|---|---|
| `Register(conf any) error` | registers the config, `conf` should be a pointer to struct |
| `Parse(conf any) error` | fills the registered config, or its field with struct type (sub config), with values from drivers |
| `ParseContext(ctx, conf any) error` | `Parse` with the context for the requests of the drivers to the remote storages |
| `Parse(ctx context.Context, conf any) error` | fills the registered config, or its field with struct type (sub config), with values from drivers; the context stops the requests of the drivers to the remote storages |
| `GenDoc(driverName string) string` | generates documentation of all registered configs in the driver format |
| `Sources(conf any) []FieldSource` | returns which driver has set the value of every field in the last `Parse` call |
| `tinyconf.Subscribe(ctx, m, onChange) error` | calls `onChange(prev, next *T)` after the changes of the config or the sub config `T` |
Expand Down Expand Up @@ -245,7 +244,7 @@ func main() {
panic(err)
}
fmt.Println(config.GenDoc("yaml"))
if err = config.Parse(c); err != nil {
if err = config.Parse(context.Background(), c); err != nil {
panic(err)
}
for _, source := range config.Sources(c) {
Expand All @@ -254,7 +253,7 @@ func main() {

// configs can be parsed partially, by the fields of the registered configs
http := &HTTP{}
if err = config.Parse(http); err != nil {
if err = config.Parse(context.Background(), http); err != nil {
panic(err)
}
fmt.Println(*http)
Expand Down Expand Up @@ -302,7 +301,7 @@ and `tinyconf.ErrIncorrectTagSettings` if the field has no settings for the driv
To support the same types as the built-in drivers, use the `convert` package: `convert.StringToField` for string values
and `convert.TreeToField` for values decoded from formats like yaml or json.

If the driver implements the optional `tinyconf.Loader` or `tinyconf.ContextLoader` interface, `Load` (or `LoadContext` with the context of `ParseContext`) is called once at the start of every `Parse` call,
If the driver implements the optional `tinyconf.Loader` or `tinyconf.ContextLoader` interface, `Load` (or `LoadContext` with the context of `Parse`) is called once at the start of every `Parse` call,
so the driver can read its source once and not for every field.
If the driver implements the optional `tinyconf.Watcher` interface, it can report changes of its source for reloading.

Expand Down
7 changes: 4 additions & 3 deletions bench_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tinyconf

import (
"context"
"testing"
"time"
)
Expand Down Expand Up @@ -35,7 +36,7 @@ func newBenchManager(b *testing.B) *Manager {
if err = c.Register(conf); err != nil {
b.Fatal(err)
}
if err = c.Parse(conf); err != nil {
if err = c.Parse(context.Background(), conf); err != nil {
b.Fatal(err)
}
return c
Expand All @@ -47,7 +48,7 @@ func BenchmarkParse(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := c.Parse(conf); err != nil {
if err := c.Parse(context.Background(), conf); err != nil {
b.Fatal(err)
}
}
Expand All @@ -59,7 +60,7 @@ func BenchmarkParse_SubConfig(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := c.Parse(http); err != nil {
if err := c.Parse(context.Background(), http); err != nil {
b.Fatal(err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/drivers/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ type Loader interface {

`Load` is called once at the start of every `Parse` call, before any `GetValue` call. If it returns an error, the driver is skipped in this `Parse` call and the error is returned from `Parse`.

A driver that reads its source by the network should implement `tinyconf.ContextLoader` instead: `LoadContext` gets the context of [`ParseContext`](../features/manager.md#parse), or the context of watching for reloads, and the requests should stop when it is done.
A driver that reads its source by the network should implement `tinyconf.ContextLoader` instead: `LoadContext` gets the context of [`Parse`](../features/manager.md#parse), or the context of watching for reloads, and the requests should stop when it is done.

```go
type ContextLoader interface {
Expand Down
2 changes: 1 addition & 1 deletion docs/features/gendoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ c := &Config{}
if err := config.Register(c); err != nil {
panic(err)
}
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
panic(err)
}
fmt.Println(config.GenDoc("yaml"))
Expand Down
11 changes: 7 additions & 4 deletions docs/features/manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,24 @@ err := config.Register(c)
## Parse

```go
err := config.Parse(c)
err := config.Parse(context.Background(), c)
```

`Parse` fills the registered config with values from drivers. The config can be a registered one or a field of the registered config with struct type, see [sub configs](structs.md#sub-configs). `tinyconf.ErrNotRegisteredConfig` is returned for all other configs.

`ParseContext` is `Parse` with the context for the drivers that read their sources by the network — [consul](../drivers/consul.md), [vault](../drivers/vault.md), [etcd](../drivers/etcd.md) and the remote documents: their requests are stopped when the context is done. A start of the application in Kubernetes is a typical case:
The context is used by the drivers that read their sources by the network — [consul](../drivers/consul.md), [vault](../drivers/vault.md), [etcd](../drivers/etcd.md) and the remote documents: their requests are stopped when the context is done. A start of the application in Kubernetes is a typical case:

```go
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := config.ParseContext(ctx, c); err != nil {
if err := config.Parse(ctx, c); err != nil {
log.Fatal(err) // errors.Is(err, context.DeadlineExceeded) if the storage is not available in time
}
```

!!! note "Migration"
`Parse(conf)` of the previous versions is `Parse(context.Background(), conf)`, and `ParseContext(ctx, conf)` is `Parse(ctx, conf)`.

Every request of the drivers is limited to 30 seconds anyway. The reloads use the context of watching, so the requests are stopped when the last subscription is canceled.

## Other methods
Expand All @@ -70,7 +73,7 @@ Every request of the drivers is limited to 30 seconds anyway. The reloads use th
`Parse` does not stop on the first error: all fields are parsed, and all errors are returned as one error. It can be checked with `errors.Is` and `errors.As`.

```go
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
// env driver: field HTTP.Port: failed to parse env value from key HTTP_PORT ...
log.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion docs/features/reloading.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ c := &Config{}
if err := config.Register(c); err != nil {
panic(err)
}
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
panic(err)
}

Expand Down
4 changes: 2 additions & 2 deletions docs/features/required.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Config struct {
```

```go
err := config.Parse(c)
err := config.Parse(context.Background(), c)
if errors.Is(err, tinyconf.ErrRequired) {
// field DSN: required value is not set; field Password: required value is not set
log.Fatal(err)
Expand All @@ -29,7 +29,7 @@ So a required field without sources can still be filled in the code before `Pars

```go
c := &Config{DSN: "postgres://localhost/dev"}
err := config.Parse(c) // DSN is not required to be in the environment
err := config.Parse(context.Background(), c) // DSN is not required to be in the environment
```

## All at once
Expand Down
2 changes: 1 addition & 1 deletion docs/features/sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
With several drivers it is not always obvious where the final value came from. `Sources` returns which driver has set every field in the last `Parse` call.

```go
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
panic(err)
}
for _, source := range config.Sources(c) {
Expand Down
4 changes: 2 additions & 2 deletions docs/features/structs.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ The pointer makes the section **optional**:
* an existing struct is filled in place, it is not replaced with a new one.

```go
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
panic(err)
}
if c.TLS != nil {
Expand All @@ -102,7 +102,7 @@ if err := config.Register(c); err != nil {

// HTTP is a type of the field in the registered Config
http := &HTTP{}
if err := config.Parse(http); err != nil {
if err := config.Parse(context.Background(), http); err != nil {
panic(err)
}
```
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func main() {
if err = config.Register(c); err != nil {
panic(err)
}
if err = config.Parse(c); err != nil {
if err = config.Parse(context.Background(), c); err != nil {
panic(err)
}
fmt.Printf("%+v\n", *c)
Expand Down
12 changes: 6 additions & 6 deletions docs/use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ c := &Config{}
if err := config.Register(c); err != nil {
panic(err)
}
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
Expand Down Expand Up @@ -384,7 +384,7 @@ type Config struct {

func New(config *tinyconf.Manager) (*Storage, error) {
c := &Config{}
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
return nil, err
}
return connect(c)
Expand Down Expand Up @@ -412,7 +412,7 @@ The package gets a [sub config](features/structs.md#sub-configs): its own fields
Check everything at start and report all problems in one message.

```go
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
if errors.Is(err, tinyconf.ErrRequired) {
fmt.Fprintln(os.Stderr, "some required settings are not set")
}
Expand All @@ -436,7 +436,7 @@ c := &Config{}
if err := config.Register(c); err != nil {
panic(err)
}
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
panic(err)
}

Expand Down Expand Up @@ -485,7 +485,7 @@ if err := config.Register(c); err != nil {
if len(os.Args) > 1 && os.Args[1] == "-doc" {
// fills the defaults and the values of this environment,
// missing required values are not a problem for the template
_ = config.Parse(c)
_ = config.Parse(context.Background(), c)
fmt.Println(config.GenDoc("yaml"))
fmt.Println(config.GenDoc("env"))
return
Expand Down Expand Up @@ -533,7 +533,7 @@ func newTestConfig(t *testing.T, yamlDoc string, args ...string) *Config {
if err := config.Register(c); err != nil {
t.Fatal(err)
}
if err := config.Parse(c); err != nil {
if err := config.Parse(context.Background(), c); err != nil {
t.Fatal(err)
}
return c
Expand Down
2 changes: 1 addition & 1 deletion docs/why-tinyconf.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Checked against the current documentation of the libraries on 2026-10-16.
| viper `SetDefault("http.port", 8080)` | `initial:"8080"` with the [tag driver](drivers/tag.md) |
| viper `AutomaticEnv()` | [`env.WithAutoKeys()`](drivers/env.md#auto-keys) |
| viper `WatchConfig()` + `OnConfigChange` | [`tinyconf.Subscribe`](features/reloading.md) per package |
| viper `Sub("http")` | `config.Parse(&HTTP{})` or `tinyconf.Current[HTTP](config)` |
| viper `Sub("http")` | `config.Parse(context.Background(), &HTTP{})` or `tinyconf.Current[HTTP](config)` |
| cleanenv `env-default`, `env-required` | `initial:"..."` and `required:"true"` |
| cleanenv `env-description` + `Usage` | `doc:"..."` + [`GenDoc("env")`](features/gendoc.md) |
| caarlos0/env `envDefault`, `required` | `initial:"..."` and `required:"true"` |
3 changes: 2 additions & 1 deletion drivers/bench_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package drivers_test

import (
"context"
"testing"
"time"

Expand Down Expand Up @@ -59,7 +60,7 @@ func BenchmarkParse_Drivers(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err = config.Parse(conf); err != nil {
if err = config.Parse(context.Background(), conf); err != nil {
b.Fatal(err)
}
}
Expand Down
9 changes: 5 additions & 4 deletions drivers/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package drivers_test

import (
"bytes"
"context"
"encoding/json"
"net"
"net/url"
Expand Down Expand Up @@ -350,7 +351,7 @@ func TestConformance(t *testing.T) {

conf := &conformanceConfig{}
assert.NoError(t, manager.Register(conf))
assert.NoError(t, manager.Parse(conf))
assert.NoError(t, manager.Parse(context.Background(), conf))

expectedOf, confOf := reflect.ValueOf(expected), reflect.ValueOf(*conf)
for i := 0; i < expectedOf.NumField(); i++ {
Expand All @@ -360,13 +361,13 @@ func TestConformance(t *testing.T) {

t.Run("sub config", func(t *testing.T) {
sub := &conformanceNested{}
assert.NoError(t, manager.Parse(sub))
assert.NoError(t, manager.Parse(context.Background(), sub))
assert.Equal(t, expected.Nested, *sub)
})

t.Run("sub config stored by pointer", func(t *testing.T) {
sub := &conformanceNestedPtr{}
assert.NoError(t, manager.Parse(sub))
assert.NoError(t, manager.Parse(context.Background(), sub))
assert.Equal(t, *expected.NestedPtr, *sub)
})
})
Expand Down Expand Up @@ -419,7 +420,7 @@ chan: 1

conf := &config{}
assert.NoError(t, manager.Register(conf))
err = manager.Parse(conf)
err = manager.Parse(context.Background(), conf)
assert.Error(t, err)
for i := 0; i < typeOf.NumField(); i++ {
assert.True(t, strings.Contains(err.Error(), "field "+typeOf.Field(i).Name+":"), typeOf.Field(i).Name)
Expand Down
2 changes: 1 addition & 1 deletion drivers/consul/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (d *consulDriver) Load() error {
return d.LoadContext(context.Background())
}

// LoadContext is Load with the context of the request, Manager calls it with the context of ParseContext.
// LoadContext is Load with the context of the request, Manager calls it with the context of Parse.
func (d *consulDriver) LoadContext(ctx context.Context) error {
pairs, index, err := d.client.load(ctx, d.listPrefix(), true)
if err != nil {
Expand Down
Loading
Loading