From 064281d7e52c1dc7a632b567cf213400af73bd2e Mon Sep 17 00:00:00 2001 From: ktong Date: Thu, 16 Nov 2023 17:56:42 -0800 Subject: [PATCH 1/4] rename global to default --- README.md | 21 +--------------- benchmark_test.go | 6 ++--- global.go => default.go | 40 +++++++++++-------------------- global_test.go => default_test.go | 6 ++--- doc.go | 39 ++++++++++-------------------- example_test.go | 8 +++---- 6 files changed, 38 insertions(+), 82 deletions(-) rename global.go => default.go (59%) rename global_test.go => default_test.go (94%) diff --git a/README.md b/README.md index 44f0d6fb..bd32f2bb 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ configuration source(s) (implementation) it actually wants to use. Something lik } } - konf.SetGlobal(config) + konf.SetDefault(config) // ... other setup code ... } @@ -89,22 +89,3 @@ There are providers for the following configuration sources: - `flag` loads configuration from flags. - `fs` loads configuration from fs.FS. - `pflag` loads configuration from [spf13/pflag](https://github.com/spf13/pflag). - -## Compatibility - -konf ensures compatibility with the current supported versions of -the [Go language](https://golang.org/doc/devel/release#policy): - -> Each major Go release is supported until there are two newer major releases. -> For example, Go 1.5 was supported until the Go 1.7 release, -> and Go 1.6 was supported until the Go 1.8 release. - -For versions of Go that are no longer supported upstream, konf will stop ensuring -compatibility with these versions in the following manner: - -- A minor release of konf will be made to add support for the new - supported release of Go. -- The following minor release of konf will remove compatibility - testing for the oldest (now archived upstream) version of Go. This, and - future, releases of konf may include features only supported by - the currently supported versions of Go. diff --git a/benchmark_test.go b/benchmark_test.go index adce201f..d5ae924d 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -21,7 +21,7 @@ func BenchmarkNew(b *testing.B) { } b.StopTimer() - konf.SetGlobal(config) + konf.SetDefault(config) assert.NoError(b, err) assert.Equal(b, "v", konf.Get[string]("k")) } @@ -30,7 +30,7 @@ func BenchmarkGet(b *testing.B) { config := konf.New() err := config.Load(mapLoader{"k": "v"}) assert.NoError(b, err) - konf.SetGlobal(config) + konf.SetDefault(config) b.ResetTimer() var value string @@ -46,7 +46,7 @@ func BenchmarkUnmarshal(b *testing.B) { config := konf.New() err := config.Load(mapLoader{"k": "v"}) assert.NoError(b, err) - konf.SetGlobal(config) + konf.SetDefault(config) b.ResetTimer() var value string diff --git a/global.go b/default.go similarity index 59% rename from global.go rename to default.go index a33fff06..7c121088 100644 --- a/global.go +++ b/default.go @@ -6,7 +6,7 @@ package konf import ( "log/slog" "reflect" - "sync" + "sync/atomic" "github.com/ktong/konf/provider/env" ) @@ -34,7 +34,7 @@ func Get[T any](path string) T { //nolint:ireturn // // The path is case-insensitive. func Unmarshal(path string, target any) error { - return getGlobal().Unmarshal(path, target) + return defaultConfig.Load().Unmarshal(path, target) } // OnChange executes the given onChange function while the value of any given path @@ -42,33 +42,21 @@ func Unmarshal(path string, target any) error { // // It requires Watch has been called. func OnChange(onChange func(), paths ...string) { - getGlobal().OnChange(func(*Config) { onChange() }, paths...) + defaultConfig.Load().OnChange(func(*Config) { onChange() }, paths...) } -// SetGlobal makes config as the global Config. After this call, -// the konf package's functions (e.g. konf.Get) will read from the global config. -// -// The default global config only loads configuration from environment variables. -// -// This method can be called multiple times but it is not concurrency-safe. -func SetGlobal(config *Config) { - global = config +// SetDefault makes c the default [Config]. +// After this call, the konf package's top functions (e.g. konf.Get) +// will read from the default config. +func SetDefault(c *Config) { + defaultConfig.Store(c) } -func getGlobal() *Config { - globalOnce.Do(func() { - if global == nil { - global = New() - // It's safe to ignore error here since env loader does not return error. - _ = global.Load(env.New()) - } - }) +var defaultConfig atomic.Pointer[Config] //nolint:gochecknoglobals - return global +func init() { //nolint:gochecknoinits + config := New() + // Ignore error as env loader does not return error. + _ = config.Load(env.New()) + defaultConfig.Store(config) } - -//nolint:gochecknoglobals -var ( - global *Config - globalOnce sync.Once -) diff --git a/global_test.go b/default_test.go similarity index 94% rename from global_test.go rename to default_test.go index 4e443dea..38f56df4 100644 --- a/global_test.go +++ b/default_test.go @@ -20,7 +20,7 @@ func TestUnmarshal(t *testing.T) { config := konf.New() err := config.Load(mapLoader{"config": "string"}) assert.NoError(t, err) - konf.SetGlobal(config) + konf.SetDefault(config) var v string assert.NoError(t, konf.Unmarshal("config", &v)) @@ -33,7 +33,7 @@ func TestGet(t *testing.T) { config := konf.New() err := config.Load(mapLoader{"config": "string"}) assert.NoError(t, err) - konf.SetGlobal(config) + konf.SetDefault(config) assert.Equal(t, "string", konf.Get[string]("config")) } @@ -42,7 +42,7 @@ func TestGet_error(t *testing.T) { config := konf.New() err := config.Load(mapLoader{"config": "string"}) assert.NoError(t, err) - konf.SetGlobal(config) + konf.SetDefault(config) buf := new(bytes.Buffer) log.SetOutput(buf) diff --git a/doc.go b/doc.go index eb29b1b5..3f128fb7 100644 --- a/doc.go +++ b/doc.go @@ -1,30 +1,17 @@ // Copyright (c) 2023 The konf authors // Use of this source code is governed by a MIT license found in the LICENSE file. -// Package konf defines a general-purpose configuration API and abstract interfaces -// to back that API. Packages in the Go ecosystem can depend on this package, -// while callers can load configuration from whatever source is appropriate. -// -// # Usage -// -// Reading configuration is done using a Config instance. Config is a concrete type -// with methods, which loads the actual configuration from a Loader interface, -// and reloads latest configuration when it has changes from a Watcher interface. -// -// Config has following main methods: -// - Config.Watch reloads configuration when it changes. -// - Config.Unmarshal loads configuration under the given path -// into the given object pointed to by target. -// - Config.OnChange register callback on configuration changes. -// -// # Global Config -// -// The following package's functions load configuration -// from the global Config while it is set by SetGlobal: -// -// - Get instances the given type and loads configuration into it. -// It returns zero value if there is an error while getting configuration. -// - Unmarshal loads configuration under the given path -// into the given object pointed to by target. -// - OnChange register callback on configuration changes. +/* +Package konf defines a general-purpose configuration API and abstract interfaces +to back that API. Packages in the Go ecosystem can depend on this package, +while callers can load configuration from whatever source is appropriate. + +It defines a type, [Config], which provides a method [Config.Unmarshal] +for loading configuration under the given path into the given object. + +Each Config is associated with multiple [Loader], +Which loads configuration from a source, such as file, environment variables etc. +There is a default Config accessible through top-level functions +(such as [Unmarshal] and [Get]) that call the corresponding Config methods. +*/ package konf diff --git a/example_test.go b/example_test.go index 4ced0f91..52df7308 100644 --- a/example_test.go +++ b/example_test.go @@ -13,14 +13,14 @@ import ( ) func ExampleGet() { - ExampleSetGlobal() + ExampleSetDefault() fmt.Print(konf.Get[string]("server.host")) // Output: example.com } func ExampleUnmarshal() { - ExampleSetGlobal() + ExampleSetDefault() cfg := struct { Host string @@ -41,7 +41,7 @@ func ExampleUnmarshal() { //go:embed testdata var testdata embed.FS -func ExampleSetGlobal() { +func ExampleSetDefault() { config := konf.New() err := config.Load( kfs.New(testdata, "testdata/config.json"), @@ -51,6 +51,6 @@ func ExampleSetGlobal() { // Handle error here. panic(err) } - konf.SetGlobal(config) + konf.SetDefault(config) // Output: } From 551ef352644b7d6e706d8a20d2b3d0d8bc8e340e Mon Sep 17 00:00:00 2001 From: ktong Date: Thu, 16 Nov 2023 19:47:14 -0800 Subject: [PATCH 2/4] rename global to default --- README.md | 10 +++++----- config.go | 35 ++++++++++++++++------------------- default.go | 18 +++++++++--------- doc.go | 8 +++++++- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index bd32f2bb..d8828476 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,8 @@ Thanks for authors of both awesome configuration libraries. There are providers for the following configuration sources: -- `env` loads configuration from environment variables. -- `file` loads configuration from a file. -- `flag` loads configuration from flags. -- `fs` loads configuration from fs.FS. -- `pflag` loads configuration from [spf13/pflag](https://github.com/spf13/pflag). +- [`env`](provider/env) loads configuration from environment variables. +- [`file`](provider/file) loads configuration from a file. +- [`flag`](provider/flag) loads configuration from flags. +- [`fs`](provider/fs) loads configuration from fs.FS. +- [`pflag`](provider/pflag) loads configuration from [spf13/pflag](https://github.com/spf13/pflag). diff --git a/config.go b/config.go index 8fe47f2f..90bba69d 100644 --- a/config.go +++ b/config.go @@ -18,7 +18,9 @@ import ( "github.com/ktong/konf/internal/maps" ) -// Config is a registry which holds configuration loaded by Loader(s). +// Config reads configuration from appropriate sources. +// +// To create a new Logger, call [New]. type Config struct { decodeHook mapstructure.DecodeHookFunc delimiter string @@ -27,11 +29,9 @@ type Config struct { values map[string]any providers []*provider - onChanges map[string][]func(*Config) - onChangesChannel chan []func(*Config) - onChangesMutex sync.RWMutex - - watchOnce sync.Once + onChanges map[string][]func(*Config) + onChangesMutex sync.RWMutex + watchOnce sync.Once } type provider struct { @@ -39,7 +39,7 @@ type provider struct { watcher Watcher } -// New returns a Config with the given Option(s). +// New creates a new Config with the given Option(s). func New(opts ...Option) *Config { option := &options{ delimiter: ".", @@ -59,7 +59,6 @@ func New(opts ...Option) *Config { } // Load loads configuration from given loaders. -// // Each loader takes precedence over the loaders before it // while multiple loaders are specified. // @@ -106,8 +105,8 @@ func (c *Config) Watch(ctx context.Context) error { //nolint:cyclop,funlen,gocog return nil } - c.onChangesChannel = make(chan []func(*Config)) - defer close(c.onChangesChannel) + onChangesChannel := make(chan []func(*Config)) + defer close(onChangesChannel) ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -118,7 +117,7 @@ func (c *Config) Watch(ctx context.Context) error { //nolint:cyclop,funlen,gocog for { select { - case onChanges := <-c.onChangesChannel: + case onChanges := <-onChangesChannel: values := make(map[string]any) for _, w := range c.providers { maps.Merge(values, w.values) @@ -167,7 +166,7 @@ func (c *Config) Watch(ctx context.Context) error { //nolint:cyclop,funlen,gocog return callbacks } - c.onChangesChannel <- onChanges() + onChangesChannel <- onChanges() } if err := provider.watcher.Watch(ctx, onChange); err != nil { errChan <- fmt.Errorf("watch configuration change: %w", err) @@ -209,10 +208,9 @@ func sub(values map[string]any, path string, delimiter string) any { return next } -// OnChange executes the given onChange function while the value of any given path -// (or any value is no paths) have been changed. -// It requires Config.Watch has been called. -// +// OnChange executes the given onChange function +// while the value of any given path have been changed. +// It requires Config.Watch has been called first. // The paths are case-insensitive. // // This method is concurrency-safe. @@ -230,9 +228,8 @@ func (c *Config) OnChange(onchange func(*Config), paths ...string) { } } -// Unmarshal loads configuration under the given path into the given object -// pointed to by target. It supports [konf] tags on struct fields for customized field name. -// +// Unmarshal reads configuration under the given path +// into the given object pointed to by target. // The path is case-insensitive. func (c *Config) Unmarshal(path string, target any) error { decoder, err := mapstructure.NewDecoder( diff --git a/default.go b/default.go index 7c121088..4676baa1 100644 --- a/default.go +++ b/default.go @@ -11,9 +11,8 @@ import ( "github.com/ktong/konf/provider/env" ) -// Get retrieves the value given the path to use. -// It returns zero value if there is an error while getting configuration. -// +// Get returns the value under the given path. +// It returns zero value if there is an error. // The path is case-insensitive. func Get[T any](path string) T { //nolint:ireturn var value T @@ -29,18 +28,19 @@ func Get[T any](path string) T { //nolint:ireturn return value } -// Unmarshal loads configuration under the given path into the given object -// pointed to by target. It supports [konf] tags on struct fields for customized field name. -// +// Unmarshal reads configuration under the given path +// into the given object pointed to by target. // The path is case-insensitive. func Unmarshal(path string, target any) error { return defaultConfig.Load().Unmarshal(path, target) } -// OnChange executes the given onChange function while the value of any given path -// (or any value is no paths) have been changed. +// OnChange executes the given onChange function +// while the value of any given path have been changed. +// It requires Config.Watch has been called first. +// The paths are case-insensitive. // -// It requires Watch has been called. +// This method is concurrency-safe. func OnChange(onChange func(), paths ...string) { defaultConfig.Load().OnChange(func(*Config) { onChange() }, paths...) } diff --git a/doc.go b/doc.go index 3f128fb7..e11f31ea 100644 --- a/doc.go +++ b/doc.go @@ -9,9 +9,15 @@ while callers can load configuration from whatever source is appropriate. It defines a type, [Config], which provides a method [Config.Unmarshal] for loading configuration under the given path into the given object. -Each Config is associated with multiple [Loader], +Each Config is associated with multiple [Loader](s), Which loads configuration from a source, such as file, environment variables etc. There is a default Config accessible through top-level functions (such as [Unmarshal] and [Get]) that call the corresponding Config methods. + +nested + +tagName + +# Watch */ package konf From 818d20955609baa8554c51a6b4b1e99be5acec91 Mon Sep 17 00:00:00 2001 From: ktong Date: Fri, 17 Nov 2023 06:53:20 -0800 Subject: [PATCH 3/4] doc --- default_test.go | 2 -- provider/env/env.go | 4 +++- provider/file/file.go | 4 +++- provider/flag/flag.go | 4 +++- provider/fs/fs.go | 4 +++- provider/pflag/pflag.go | 4 +++- 6 files changed, 15 insertions(+), 7 deletions(-) diff --git a/default_test.go b/default_test.go index 38f56df4..f8e2b89a 100644 --- a/default_test.go +++ b/default_test.go @@ -1,8 +1,6 @@ // Copyright (c) 2023 The konf authors // Use of this source code is governed by a MIT license found in the LICENSE file. -//go:build !race - package konf_test import ( diff --git a/provider/env/env.go b/provider/env/env.go index 6220aa3a..d394b271 100644 --- a/provider/env/env.go +++ b/provider/env/env.go @@ -21,13 +21,15 @@ import ( ) // Env is a Provider that loads configuration from environment variables. +// +// To create a new Env, call [New]. type Env struct { _ [0]func() // Ensure it's incomparable. prefix string delimiter string } -// New returns an Env with the given Option(s). +// New creates an Env with the given Option(s). func New(opts ...Option) Env { option := &options{ delimiter: "_", diff --git a/provider/file/file.go b/provider/file/file.go index 3a88aca3..f751c2f1 100644 --- a/provider/file/file.go +++ b/provider/file/file.go @@ -20,13 +20,15 @@ import ( ) // File is a Provider that loads configuration from file. +// +// To create a new File, call [New]. type File struct { path string unmarshal func([]byte, any) error ignoreNotExist bool } -// New returns a File with the given path and Option(s). +// New creates a File with the given path and Option(s). func New(path string, opts ...Option) File { option := &options{ path: path, diff --git a/provider/flag/flag.go b/provider/flag/flag.go index d0f0d691..254a837e 100644 --- a/provider/flag/flag.go +++ b/provider/flag/flag.go @@ -24,6 +24,8 @@ import ( ) // Flag is a Provider that loads configuration from flags. +// +// To create a new Flag, call [New]. type Flag struct { _ [0]func() // Ensure it's incomparable. set *flag.FlagSet @@ -31,7 +33,7 @@ type Flag struct { prefix string } -// New returns a Flag with the given Option(s). +// New creates a Flag with the given Option(s). func New(opts ...Option) Flag { option := &options{ delimiter: ".", diff --git a/provider/fs/fs.go b/provider/fs/fs.go index dda26f60..3cf40149 100644 --- a/provider/fs/fs.go +++ b/provider/fs/fs.go @@ -21,6 +21,8 @@ import ( ) // FS is a Provider that loads configuration from file system. +// +// To create a new FS, call [New]. type FS struct { unmarshal func([]byte, any) error fs fs.FS @@ -28,7 +30,7 @@ type FS struct { ignoreNotExist bool } -// New returns a FS with the given fs.FS, path and Option(s). +// New creates a FS with the given fs.FS, path and Option(s). func New(fs fs.FS, path string, opts ...Option) FS { option := &options{ fs: fs, diff --git a/provider/pflag/pflag.go b/provider/pflag/pflag.go index f629c30b..477eaf4e 100644 --- a/provider/pflag/pflag.go +++ b/provider/pflag/pflag.go @@ -26,6 +26,8 @@ import ( ) // PFlag is a Provider that loads configuration from flags defined by [spf13/pflag]. +// +// To create a new PFlag, call [New]. type PFlag struct { _ [0]func() // Ensure it's incomparable. set *pflag.FlagSet @@ -33,7 +35,7 @@ type PFlag struct { prefix string } -// New returns a PFlag with the given Option(s). +// New creates a PFlag with the given Option(s). func New(opts ...Option) PFlag { option := &options{ delimiter: ".", From 7e5572ba85b5988bf4834ca29ded786f16938d3d Mon Sep 17 00:00:00 2001 From: ktong Date: Fri, 17 Nov 2023 07:07:06 -0800 Subject: [PATCH 4/4] doc --- README.md | 4 ++-- doc.go | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d8828476..d0df6992 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# A minimal configuration API for Go +# A minimalist configuration API for Go [![Go Reference](https://pkg.go.dev/badge/github.com/ktong/konf.svg)](https://pkg.go.dev/github.com/ktong/konf) [![Build](https://github.com/ktong/konf/actions/workflows/test.yml/badge.svg)](https://github.com/ktong/konf/actions/workflows/test.yml) @@ -19,7 +19,7 @@ This decoupling allows application developers to write code in terms of `*konf.C while the configuration source(s) is managed "up stack" (e.g. in or near `main()`). Application developers can then switch configuration sources(s) as necessary. -## Typical usage +## Usage Somewhere, early in an application's life, it will make a decision about which configuration source(s) (implementation) it actually wants to use. Something like: diff --git a/doc.go b/doc.go index e11f31ea..b824012b 100644 --- a/doc.go +++ b/doc.go @@ -14,10 +14,15 @@ Which loads configuration from a source, such as file, environment variables etc There is a default Config accessible through top-level functions (such as [Unmarshal] and [Get]) that call the corresponding Config methods. -nested +Configuration is hierarchical, and the path is a sequence of keys that separated by delimiter. +The default delimiter is `.`, which makes configuration path like `parent.child.key`. -tagName +# Watch Changes -# Watch +[Config.Watch] watches and updates configuration when it changes, which leads [Config.Unmarshal] +always returns latest configuration. + +You may use [Config.OnChange] to register a callback if the value of any path have been changed. +It could push the change into application objects instead pulling the configuration periodically. */ package konf