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

refine go doc #67

Merged
merged 1 commit into from
Dec 15, 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
15 changes: 7 additions & 8 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

// Config reads configuration from appropriate sources.
//
// To create a new Logger, call [New].
// To create a new Config, call [New].
type Config struct {
decodeHook mapstructure.DecodeHookFunc
delimiter string
Expand Down Expand Up @@ -59,9 +59,8 @@ func New(opts ...Option) *Config {
return (*Config)(option)
}

// Load loads configuration from given loaders.
// Each loader takes precedence over the loaders before it
// while multiple loaders are specified.
// Load loads configuration from the given loaders.
// Each loader takes precedence over the loaders before it.
//
// This method can be called multiple times but it is not concurrency-safe.
func (c *Config) Load(loaders ...Loader) error {
Expand Down Expand Up @@ -216,8 +215,8 @@ 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 have been changed.
// OnChange registers a callback function that is executed
// when the value of any given path in the Config changes.
// It requires Config.Watch has been called first.
// The paths are case-insensitive.
//
Expand All @@ -236,8 +235,8 @@ func (c *Config) OnChange(onchange func(*Config), paths ...string) {
}
}

// Unmarshal reads configuration under the given path
// into the given object pointed to by target.
// Unmarshal reads configuration under the given path from the Config
// and decodes it 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
17 changes: 8 additions & 9 deletions default.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"github.com/ktong/konf/provider/env"
)

// Get returns the value under the given path.
// It returns zero value if there is an error.
// Get retrieves the value under the given path from the default Config.
// It returns the zero value of the expected type if there is an error.
// The path is case-insensitive.
func Get[T any](path string) T { //nolint:ireturn
var value T
Expand All @@ -28,26 +28,25 @@ func Get[T any](path string) T { //nolint:ireturn
return value
}

// Unmarshal reads configuration under the given path
// into the given object pointed to by target.
// Unmarshal reads configuration under the given path from the default Config
// and decodes it 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.
// OnChange registers a callback function that is executed
// when the value of any given path in the default Config changes.
// The paths are case-insensitive.
//
// This method is concurrency-safe.
func OnChange(onChange func(), paths ...string) {
defaultConfig.Load().OnChange(func(*Config) { onChange() }, paths...)
}

// SetDefault makes c the default [Config].
// SetDefault sets the given Config as the default Config.
// After this call, the konf package's top functions (e.g. konf.Get)
// will read from the default config.
// will interact with the given Config.
func SetDefault(c *Config) {
defaultConfig.Store(c)
}
Expand Down
11 changes: 9 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@
// 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
Package konf provides 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.
Which load 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`.

# Load Configuration
After creating a [Config], you can load configuration from multiple [Loader](s) using [Config.Load].
Each loader takes precedence over the loaders before it. As long as the configuration has been loaded,
it can be used in following code to get or unmarshal configuration, even for loading configuration
from another source. For example, it can read config file path from environment variables,
and then use the file path to load configuration from file system.

# Watch Changes

[Config.Watch] watches and updates configuration when it changes, which leads [Config.Unmarshal]
Expand Down
14 changes: 10 additions & 4 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,38 @@ package konf

import "github.com/mitchellh/mapstructure"

// WithDelimiter provides the delimiter when specifying config path.
// WithDelimiter provides the delimiter used when specifying config paths.
// The delimiter is used to separate keys in the path.
//
// The default delimiter is `.`, which makes config path like `parent.child.key`.
// For example, with the default delimiter `.`, a config path might look like `parent.child.key`.
func WithDelimiter(delimiter string) Option {
return func(options *options) {
options.delimiter = delimiter
}
}

// WithTagName provides the tag name that [mapstructure] reads for field names.
// The tag name is used by mapstructure when decoding configuration into structs.
//
// The default tag name is `konf`.
// For example, with the default tag name `konf`, mapstructure would look for `konf` tags on struct fields.
func WithTagName(tagName string) Option {
return func(options *options) {
options.tagName = tagName
}
}

// WithDecodeHook provides the decode hook for [mapstructure] decoding.
// The decode hook is a function that can transform or customize how values are decoded.
//
// By default, it composes mapstructure.StringToTimeDurationHookFunc,
// mapstructure.StringToSliceHookFunc(",") and mapstructure.TextUnmarshallerHookFunc.
func WithDecodeHook(decodeHook mapstructure.DecodeHookFunc) Option {
return func(options *options) {
options.decodeHook = decodeHook
}
}

// Option configures the given Config.
// Option configures a Config with specific options.
type Option func(*options)

type options Config
8 changes: 4 additions & 4 deletions provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import "context"

// Loader is the interface that wraps the Load method.
//
// Load loads latest configuration and returns as a nested map[string]any.
// It requires that the string keys should be nested like `{parent: {child: {key: 1}}}`.
// The key in returned map should be case-insensitive, otherwise random overridden exists.
// Load loads the latest configuration and returns it as a nested map[string]any.
// The keys in the returned map should be case-insensitive to avoid random overriding.
// The keys should be nested like `{parent: {child: {key: 1}}}`.
type Loader interface {
Load() (map[string]any, error)
}

// Watcher is the interface that wraps the Watch method.
//
// Watch watches configuration and triggers onChange callback with latest
// Watch watches the configuration and triggers the onChange callback with the latest
// full configurations as a nested map[string]any when it changes.
// It blocks until ctx is done, or the watching returns an error.
type Watcher interface {
Expand Down
14 changes: 6 additions & 8 deletions provider/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@

// Package env loads configuration from environment variables.
//
// Env loads all environment variables and returns nested map[string]any.
// by splitting the names by `_`. E.g. the environment variable
// `PARENT_CHILD_KEY="1"` is loaded as `{PARENT: {CHILD: {KEY: "1"}}}`.
// The environment variables with empty value are treated as unset.
// Env loads environment variables whose names starts with the given prefix
// and returns them as a nested map[string]any.
// Environment variables with empty values are treated as unset.
//
// The default behavior can be changed with following options:
// - WithPrefix enables loads environment variables with the given prefix in the name.
// - WithDelimiter provides the delimiter when splitting environment variable name to nested keys.
// It splits the names by delimiter. For example, with the default delimiter "_",
// the environment variable `PARENT_CHILD_KEY="1"` is loaded as `{PARENT: {CHILD: {KEY: "1"}}}`.
package env

import (
Expand All @@ -22,7 +20,7 @@ import (

// Env is a Provider that loads configuration from environment variables.
//
// To create a new Env, call [New].
// To create a new Env, call New.
type Env struct {
_ [0]func() // Ensure it's incomparable.
prefix string
Expand Down
15 changes: 8 additions & 7 deletions provider/env/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@

package env

// WithPrefix enables loads environment variables with the given prefix in the name.
// WithPrefix provides the prefix used when loading environment variables.
// Only environment variables with names that start with the prefix will be loaded.
//
// E.g. if the given prefix is "server", it only loads environment variables
// which name starts with "server".
// For example, if the prefix is "server", only environment variables whose names start with "server" will be loaded.
// By default, it has no prefix which loads all environment variables.
func WithPrefix(prefix string) Option {
return func(options *options) {
options.prefix = prefix
}
}

// WithDelimiter provides the delimiter when splitting environment variable name to nested keys.
// WithDelimiter provides the delimiter used when splitting environment variable names into nested keys.
//
// The default delimiter is `_`, which loads the environment variable `PARENT_CHILD_KEY="1"`
// as `{PARENT: {CHILD: {KEY: "1"}}}`.
// For example, with the default delimiter "_", an environment variable name like "PARENT_CHILD_KEY"
// would be split into "PARENT", "CHILD", and "KEY".
func WithDelimiter(delimiter string) Option {
return func(options *options) {
options.delimiter = delimiter
}
}

// Option configures the given Env.
// Option configures an Env with specific options.
type Option func(*options)

type options Env
17 changes: 9 additions & 8 deletions provider/file/file.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
// Copyright (c) 2023 The konf authors
// Use of this source code is governed by a MIT license found in the LICENSE file.

// Package file loads configuration from files.
// Package file loads configuration from OS file.
//
// File loads file with given path from OS file system
// and returns nested map[string]any that is parsed as json.
// File loads a file with the given path from the OS file system and returns
// a nested map[string]any that is parsed with the given unmarshal function.
//
// The default behavior can be changed with following options:
// - WithUnmarshal provides the function that parses config file.
// E.g. `WithUnmarshal(yaml.Unmarshal)` will parse the file as yaml.
// - IgnoreFileNotExit ignores the error if config file does not exist.
// The unmarshal function must be able to unmarshal the file content into a map[string]any.
// For example, with the default json.Unmarshal, the file is parsed as JSON.
//
// By default, it returns error while loading if the file is not found.
// IgnoreFileNotExit can override the behavior to return an empty map[string]any.
package file

import (
Expand All @@ -19,7 +20,7 @@ import (
"os"
)

// File is a Provider that loads configuration from file.
// File is a Provider that loads configuration from a OS file.
//
// To create a new File, call [New].
type File struct {
Expand Down
61 changes: 0 additions & 61 deletions provider/file/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
package file_test

import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"

"github.com/ktong/konf/provider/file"
"github.com/ktong/konf/provider/file/internal/assert"
Expand Down Expand Up @@ -76,62 +71,6 @@ func TestFile_Load(t *testing.T) {
}
}

func TestFile_Watch(t *testing.T) {
testcases := []struct {
description string
action func(string) error
expacted map[string]any
}{
{
description: "create",
action: func(path string) error {
return os.WriteFile(path, []byte(`{"p": {"k": "v"}}`), 0o600)
},
expacted: map[string]any{"p": map[string]any{"k": "v"}},
},
{
description: "write",
action: func(path string) error {
return os.WriteFile(path, []byte(`{"p": {"k": "c"}}`), 0o600)
},
expacted: map[string]any{"p": map[string]any{"k": "c"}},
},
{
description: "remove",
action: os.Remove,
},
}

for i := range testcases {
testcase := testcases[i]

t.Run(testcase.description, func(t *testing.T) {
tmpFile := filepath.Join(t.TempDir(), "watch.json")
assert.NoError(t, os.WriteFile(tmpFile, []byte(`{"p": {"k": "v"}}`), 0o600))

loader := file.New(tmpFile)
var values map[string]any
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

var waitGroup sync.WaitGroup
waitGroup.Add(1)
go func() {
err := loader.Watch(ctx, func(changed map[string]any) {
defer waitGroup.Done()
values = changed
})
assert.NoError(t, err)
}()

time.Sleep(time.Second)
assert.NoError(t, testcase.action(tmpFile))
waitGroup.Wait()
assert.Equal(t, testcase.expacted, values)
})
}
}

func TestFile_String(t *testing.T) {
t.Parallel()

Expand Down
7 changes: 4 additions & 3 deletions provider/file/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

package file

// WithUnmarshal provides the function that parses config file.
// WithUnmarshal provides the function used to parses the configuration file.
// The unmarshal function must be able to unmarshal the file content into a map[string]any.
//
// The default function is json.Unmarshal.
func WithUnmarshal(unmarshal func([]byte, any) error) Option {
Expand All @@ -12,14 +13,14 @@ func WithUnmarshal(unmarshal func([]byte, any) error) Option {
}
}

// IgnoreFileNotExit ignores the error if config file does not exist.
// IgnoreFileNotExit ignores the error and return an empty map instead if the configuration file is not found.
func IgnoreFileNotExit() Option {
return func(options *options) {
options.ignoreNotExist = true
}
}

// Option configures the given File.
// Option configures the a File with specific options.
type Option func(options *options)

type options File
3 changes: 0 additions & 3 deletions provider/file/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import (
"github.com/fsnotify/fsnotify"
)

// Watch watches the file and triggers a callback when it changes.
// It blocks until ctx is done, or the service returns a non-retryable error.
//
//nolint:cyclop,funlen
func (f File) Watch(ctx context.Context, onChange func(map[string]any)) error {
watcher, err := fsnotify.NewWatcher()
Expand Down
Loading
Loading