Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor doc #62

Merged
merged 4 commits into from
Nov 17, 2023
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
35 changes: 8 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -47,7 +47,7 @@ configuration source(s) (implementation) it actually wants to use. Something lik
}
}

konf.SetGlobal(config)
konf.SetDefault(config)

// ... other setup code ...
}
Expand Down Expand Up @@ -84,27 +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).

## 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.
- [`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).
6 changes: 3 additions & 3 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Expand All @@ -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
Expand All @@ -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
Expand Down
35 changes: 16 additions & 19 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,19 +29,17 @@ 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 {
values map[string]any
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: ".",
Expand All @@ -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.
//
Expand Down Expand Up @@ -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()

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand Down
62 changes: 62 additions & 0 deletions default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2023 The konf authors
// Use of this source code is governed by a MIT license found in the LICENSE file.

package konf

import (
"log/slog"
"reflect"
"sync/atomic"

"github.com/ktong/konf/provider/env"
)

// 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
if err := Unmarshal(path, &value); err != nil {
slog.Error(
"Could not read config, return empty value instead.",
"error", err,
"path", path,
"type", reflect.TypeOf(value),
)
}

return value
}

// 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 have been changed.
// It requires Config.Watch has been called first.
// The paths are case-insensitive.
//
// This method is concurrency-safe.
func OnChange(onChange func(), paths ...string) {
defaultConfig.Load().OnChange(func(*Config) { onChange() }, paths...)

Check warning on line 45 in default.go

View check run for this annotation

Codecov / codecov/patch

default.go#L44-L45

Added lines #L44 - L45 were not covered by tests
}

// 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)
}

var defaultConfig atomic.Pointer[Config] //nolint:gochecknoglobals

func init() { //nolint:gochecknoinits
config := New()
// Ignore error as env loader does not return error.
_ = config.Load(env.New())
defaultConfig.Store(config)
}
8 changes: 3 additions & 5 deletions global_test.go → default_test.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -20,7 +18,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))
Expand All @@ -33,7 +31,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"))
}
Expand All @@ -42,7 +40,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)
Expand Down
50 changes: 24 additions & 26 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
// 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](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.

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`.

# Watch Changes

[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
8 changes: 4 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"),
Expand All @@ -51,6 +51,6 @@ func ExampleSetGlobal() {
// Handle error here.
panic(err)
}
konf.SetGlobal(config)
konf.SetDefault(config)
// Output:
}
Loading
Loading