From 407bf4c2c70bda20dfd6174e114a3a351059221a Mon Sep 17 00:00:00 2001 From: ktong Date: Fri, 15 Dec 2023 13:36:30 +0800 Subject: [PATCH] refine go doc --- config.go | 15 ++++---- default.go | 17 ++++----- doc.go | 11 +++++- option.go | 14 +++++-- provider.go | 8 ++-- provider/env/env.go | 14 +++---- provider/env/option.go | 15 ++++---- provider/file/file.go | 17 +++++---- provider/file/file_test.go | 61 ------------------------------ provider/file/option.go | 7 ++-- provider/file/watch.go | 3 -- provider/file/watch_test.go | 74 +++++++++++++++++++++++++++++++++++++ provider/flag/flag.go | 13 +++---- provider/flag/option.go | 15 ++++---- provider/fs/fs.go | 13 ++++--- provider/fs/option.go | 7 ++-- provider/pflag/option.go | 15 ++++---- provider/pflag/pflag.go | 11 ++---- 18 files changed, 175 insertions(+), 155 deletions(-) create mode 100644 provider/file/watch_test.go diff --git a/config.go b/config.go index e2ac065c..fcaeb3ad 100644 --- a/config.go +++ b/config.go @@ -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 @@ -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 { @@ -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. // @@ -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( diff --git a/default.go b/default.go index 4676baa1..5338ee85 100644 --- a/default.go +++ b/default.go @@ -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 @@ -28,16 +28,15 @@ 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. @@ -45,9 +44,9 @@ 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) } diff --git a/doc.go b/doc.go index b824012b..b6e29654 100644 --- a/doc.go +++ b/doc.go @@ -2,7 +2,7 @@ // 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. @@ -10,13 +10,20 @@ 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] diff --git a/option.go b/option.go index 15cfc02a..e5316fb1 100644 --- a/option.go +++ b/option.go @@ -5,9 +5,10 @@ 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 @@ -15,8 +16,9 @@ func WithDelimiter(delimiter string) Option { } // 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 @@ -24,13 +26,17 @@ func WithTagName(tagName string) Option { } // 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 diff --git a/provider.go b/provider.go index 953368aa..a31850dc 100644 --- a/provider.go +++ b/provider.go @@ -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 { diff --git a/provider/env/env.go b/provider/env/env.go index d394b271..65e03501 100644 --- a/provider/env/env.go +++ b/provider/env/env.go @@ -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 ( @@ -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 diff --git a/provider/env/option.go b/provider/env/option.go index b838a990..e2d3734c 100644 --- a/provider/env/option.go +++ b/provider/env/option.go @@ -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 diff --git a/provider/file/file.go b/provider/file/file.go index f751c2f1..088311e2 100644 --- a/provider/file/file.go +++ b/provider/file/file.go @@ -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 ( @@ -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 { diff --git a/provider/file/file_test.go b/provider/file/file_test.go index 6777a810..3fdc89be 100644 --- a/provider/file/file_test.go +++ b/provider/file/file_test.go @@ -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" @@ -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() diff --git a/provider/file/option.go b/provider/file/option.go index cf9c2504..00c6890b 100644 --- a/provider/file/option.go +++ b/provider/file/option.go @@ -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 { @@ -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 diff --git a/provider/file/watch.go b/provider/file/watch.go index a01aa518..586a523d 100644 --- a/provider/file/watch.go +++ b/provider/file/watch.go @@ -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() diff --git a/provider/file/watch_test.go b/provider/file/watch_test.go new file mode 100644 index 00000000..27bab8e2 --- /dev/null +++ b/provider/file/watch_test.go @@ -0,0 +1,74 @@ +// 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 file_test + +import ( + "context" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/ktong/konf/provider/file" + "github.com/ktong/konf/provider/file/internal/assert" +) + +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) + }) + } +} diff --git a/provider/flag/flag.go b/provider/flag/flag.go index 254a837e..075ccb4d 100644 --- a/provider/flag/flag.go +++ b/provider/flag/flag.go @@ -1,18 +1,15 @@ // Copyright (c) 2023 The konf authors // Use of this source code is governed by a MIT license found in the LICENSE file. -// Package flag loads configuration from flags. +// Package flag loads configuration from flags defined by [flag]. // -// Flag loads all flags in [flag.CommandLine] and returns nested map[string]any. -// by splitting the names by `.`.E.g. the flag `parent.child.key` with value 1 -// is loaded as `{parent: {child: {key: 1}}}`. +// Flag loads flags in [flag.CommandLine] whose names starts with the given prefix +// and returns them as a nested map[string]any. // The unchanged flags with zero default value are skipped to avoid // overriding values set by other loader. // -// The default behavior can be changed with following options: -// - WithPrefix enables loads flags with the given prefix in the name. -// - WithFlagSet provides the flag set that loads configuration from. -// - WithDelimiter provides the delimiter when splitting flag name to nested keys. +// It splits the names by delimiter. For example, with the default delimiter ".", +// the flag `parent.child.key="1"` is loaded as `{parent: {child: {key: "1"}}}`. package flag import ( diff --git a/provider/flag/option.go b/provider/flag/option.go index d0e506c1..f80b5451 100644 --- a/provider/flag/option.go +++ b/provider/flag/option.go @@ -5,10 +5,11 @@ package flag import "flag" -// WithPrefix enables only loads flags with the given prefix in the name. +// WithPrefix provides the prefix used when loading flags. +// Only flags with names that start with the prefix will be loaded. // -// E.g. if the given prefix is "server", it only loads flags -// which name starts with "server". +// For example, if the prefix is "server", only flags whose names start with "server" will be loaded. +// By default, it has no prefix which loads all flags. func WithPrefix(prefix string) Option { return func(options *options) { options.prefix = prefix @@ -24,17 +25,17 @@ func WithFlagSet(set *flag.FlagSet) Option { } } -// WithDelimiter provides the delimiter when splitting flag name to nested keys. +// WithDelimiter provides the delimiter used when splitting flag names into nested keys. // -// The default delimiter is `_`, which loads the flag `parent.child.key` with value 1 -// as `{parent: {child: {key: 1}}}`. +// For example, with the default delimiter ".", an flag 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 give Flag. +// Option configures the a Flag with specific options. type Option func(*options) type options Flag diff --git a/provider/fs/fs.go b/provider/fs/fs.go index 3cf40149..6f695635 100644 --- a/provider/fs/fs.go +++ b/provider/fs/fs.go @@ -3,13 +3,14 @@ // Package fs loads configuration from file system. // -// FS loads file with given path from file system -// and returns nested map[string]any that is parsed as json. +// FS loads a file with the given path from the 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 fs import ( diff --git a/provider/fs/option.go b/provider/fs/option.go index a50c7bed..9428d372 100644 --- a/provider/fs/option.go +++ b/provider/fs/option.go @@ -3,7 +3,8 @@ package fs -// 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 { @@ -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 FS. +// Option configures the a FS with specific options. type Option func(file *options) type options FS diff --git a/provider/pflag/option.go b/provider/pflag/option.go index ad9237c7..b3a4f732 100644 --- a/provider/pflag/option.go +++ b/provider/pflag/option.go @@ -5,10 +5,11 @@ package pflag import "github.com/spf13/pflag" -// WithPrefix enables only loads flags with the given prefix in the name. +// WithPrefix provides the prefix used when loading flags. +// Only flags with names that start with the prefix will be loaded. // -// E.g. if the given prefix is "server", it only loads flags -// which name starts with "server". +// For example, if the prefix is "server", only flags whose names start with "server" will be loaded. +// By default, it has no prefix which loads all flags. func WithPrefix(prefix string) Option { return func(options *options) { options.prefix = prefix @@ -24,17 +25,17 @@ func WithFlagSet(set *pflag.FlagSet) Option { } } -// WithDelimiter provides the delimiter when splitting flag name to nested keys. +// WithDelimiter provides the delimiter used when splitting flag names into nested keys. // -// The default delimiter is `_`, which loads the flag `parent.child.key` with value 1 -// as `{parent: {child: {key: 1}}}`. +// For example, with the default delimiter ".", an flag 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 give PFlag. +// Option configures the a PFlag with specific options. type Option func(*options) type options PFlag diff --git a/provider/pflag/pflag.go b/provider/pflag/pflag.go index 477eaf4e..7b615b7c 100644 --- a/provider/pflag/pflag.go +++ b/provider/pflag/pflag.go @@ -3,16 +3,13 @@ // Package pflag loads configuration from flags defined by [spf13/pflag]. // -// PFlag loads all flags in [pflag.CommandLine] and returns nested map[string]any. -// by splitting the names by `.`.E.g. the flag `parent.child.key` with value 1 -// is loaded as `{parent: {child: {key: 1}}}`. +// PFlag loads flags in [pflag.CommandLine] whose names starts with the given prefix +// and returns them as a nested map[string]any. // The unchanged flags with zero default value are skipped to avoid // overriding values set by other loader. // -// The default behavior can be changed with following options: -// - WithPrefix enables loads flags with the given prefix in the name. -// - WithFlagSet provides the flag set that loads configuration from. -// - WithDelimiter provides the delimiter when splitting flag name to nested keys. +// It splits the names by delimiter. For example, with the default delimiter ".", +// the flag `parent.child.key="1"` is loaded as `{parent: {child: {key: "1"}}}`. package pflag import (