Package xconf
provides a configuration registry for an application.
Configurations can be extracted from a file / env / flag set / remote system.
Supported formats are json, yaml, ini, (java) properties, toml, plain.
$ go get -u github.com/actforgood/xconf
You can create your own configuration retriever implementing Loader
interface.
Package provides these Loaders for you:
EnvLoader
- loads environment variables.DotEnvFileLoader
,DotEnvReaderLoader
- loads configuration from a .env file /io.Reader
.JSONFileLoader
,JSONReaderLoader
- loads json configuration from a file /io.Reader
.YAMLFileLoader
,YAMLReaderLoader
- loads yaml configuration from a file /io.Reader
.IniFileLoader
- loads ini configuration from a file.PropertiesFileLoader
,PropertiesBytesLoader
- loads java style properties configuration from a file / bytes slice.TOMLFileLoader
,TOMLReaderLoader
- loads toml configuration from a file /io.Reader
.ConsulLoader
- loads json/yaml/plain configuration from a remote Consul KV Store.EtcdLoader
- loads json/yaml/plain configuration from a remote Etcd KV Store.PlainLoader
- explicit configuration provider.FileLoader
- factory for<JSON|YAML|Ini|DotEnv|Properties|TOML>FileLoader
s based on file extension.FlagSetLoader
- extracts configuration from aflag.FlagSet
.MultiLoader
- loads (and merges, if configured) configuration from multiple loaders.
Upon above loaders there are available decorators which can help you achieve more sophisticated outcome:
FilterKVLoader
- filters other loader's configurations (based on keys and or their values).
Example of applicability: I load configurations from environment, but I only want the ones prefixed with "MY_APP_" - I can apply this loader withFilterKVWhitelistFunc(FilterKeyWithPrefix("MY_APP_")
filter function.AlterValueLoader
- changes the value for a configuration key.
Example of applicability: I load configurations from environment and for a given key I want its value to be a slice (not a string as envs are read/stored by default) - I can apply this loader withToStringList
altering function.IgnoreErrorLoader
- ignores the error returned by another loader.
Example of applicability: I load configuration from environment and from file (using aMultiLoader
), but it's not mandatory for that file to exist (file it's just an auxiliary source for my configurations, that may exist) - I can use this loader to ignore "file does not exist" error.FileCacheLoader
- caches configuration from a[X]FileLoader
until file gets modified (to be used if loader is called multiple times).FlattenLoader
- creates easy to access nested configuration leaf keys symlinks.AliasLoader
- creates aliases for other keys.
The main configuration contract this package provides looks like:
type Config interface {
Get(key string, def ...any) any
}
with a default implementation obtained with:
// NewDefaultConfig instantiates a new default config object.
// The first parameter is the loader used as a source of getting the key-value configuration map.
// The second parameter represents a list of optional functions to configure the object.
func NewDefaultConfig(loader Loader, opts ...DefaultConfigOption) (*DefaultConfig, error)
The DefaultConfig
has an option of reloading configurations (interval based), if you want to retrieve updated configuration
at runtime.
There are 2 (proposed) ways of working with it:
- injecting a
Config
reference and callingGet(key)
every time you need a configuration. - registering your class as an observer to get notified about config changes.
Example of usage (first case) (note: code does not compile):
// cart_service.go
const (
defaultMaxQtyCfgVal uint = 100
maxQtyCfgKey = "MAX_ALLOWED_QTY_TO_ORDER"
)
type CartService struct {
config xconf.Config
}
func NewCartService(config xconf.Config) *CartService {
return &CartService{
config: config,
}
}
func (cartSvc *CartService) AddProduct(sku string, qty uint) error {
// ...
if customerType != B2B {
totalQty := currentQty + qty
maxQty := cartSvc.config.Get(maxQtyCfgKey, defaultMaxQtyCfgVal).(uint)
if totalQty > maxQty {
return ErrMaxQtyExceeded
}
}
// ...
return nil
}
func main() {
// somewhere in the bootstrap of your application ...
var (
loader xconf.Loader // = ... your desired source(s)
config xconf.Config
cartSvc *CartService
)
config, err := xconf.NewDefaultConfig(
loader,
xconf.DefaultConfigWithReloadInterval(time.Minute), // reload every minute
)
if err != nil {
panic(err)
}
cartSvc = NewCartService(config)
// somewhere in the application business flow ...
_ = cartSvc.AddProduct("IPHONE", 1)
// somewhere in the shutdown of your application ...
if closableConfig, ok := config.(io.Closer); ok {
_ = closableConfig.Close()
}
}
Example of usage (second case) (note: code does not compile):
// redis_wrapper.go
const (
RedisHostCfgKey = "REDIS_HOST"
DefaultRedisHostCfgVal = "127.0.0.1:6379"
)
type RedisClient interface {
Ping() error
Get(key string) (string, error)
Set(key string, value any, expiration time.Duration) (string, error)
Close() error
}
type RedisClientWrapper struct {
client *redis.Client // official client
mu sync.RWMutex
}
func NewRedisClientWrapper(host string) *RedisClientWrapper {
officialClient = ...
return &RedisClientWrapper {
client: officialClient,
}
}
func (wrapper *RedisClientWrapper) Get(key string) (string, error) {
wrapper.mu.RLock()
defer wrapper.mu.RUnlock()
return wrapper.client.Get(key).Result()
}
func (wrapper *RedisClientWrapper) OnConfigChange(config xconf.Config, changedKeys ...string) {
for _, changedKey := range changedKeys {
if changedKey == RedisHostCfgKey { // or use strings.EqualFold() if you enabled DefaultConfigWithIgnoreCaseSensitivity.
wrapper.mu.Lock()
_ = wrapper.client.Close() // close previous client
newClient := ... // reinitialize client based on config.Get(RedisHostCfgKey).(string)
wrapper.client = newClient
wrapper.mu.Unlock()
}
}
}
func main() {
// somewhere in the bootstrap of your application ...
var (
loader xconf.Loader // = ... your desired source(s)
config xconf.Config
redisClient RedisClient
)
config, err := xconf.NewDefaultConfig(
loader,
xconf.DefaultConfigWithReloadInterval(30 * time.Second), // reload every 30 seconds
)
if err != nil {
panic(err)
}
redisHost := config.Get(RedisHostCfgKey, DefaultRedisHostCfgVal).(string)
redisClient = NewRedisClient(redisHost)
config.RegisterObserver(redisClient.OnConfigChange) // register redis wrapper as an observer
// somewhere in the application business flow ...
_, _ = redisClient.Get("something")
// somewhere in the shutdown of your application ...
if closableConfig, ok := config.(io.Closer); ok {
_ = closableConfig.Close()
}
_ = redisClient.Close()
}
This is not the subject of this package, but as a mention, you can achieve that if needed, with a package like github.com/mitchellh/mapstructure.
Example:
package main
import (
"bytes"
"fmt"
"github.com/actforgood/xconf"
"github.com/mitchellh/mapstructure"
)
type DBConfig struct {
Host string
Port int
Auth Auth
}
type Auth struct {
Username string
Password string
}
func main() {
var (
jsonConfig = `{
"db": {
"host": "127.0.0.1",
"port": 3306,
"auth": {
"username": "JohnDoe",
"password": "verySecretPwd"
}
}
}`
dbConfig DBConfig // the struct to populate with configuration
dbConfigMap map[string]any // the configuration map for "db" key
loader = xconf.JSONReaderLoader(bytes.NewReader([]byte(jsonConfig)))
)
// example using directly a Loader:
configMap, err := loader.Load()
if err != nil {
panic(err)
}
dbConfigMap = configMap["db"].(map[string]any)
if err := mapstructure.Decode(dbConfigMap, &dbConfig); err != nil {
panic(err)
}
fmt.Printf("%+v", dbConfig)
// example using the Config contract:
config, err := xconf.NewDefaultConfig(loader)
if err != nil {
panic(err)
}
dbConfigMap = config.Get("db").(map[string]any)
if err := mapstructure.Decode(dbConfigMap, &dbConfig); err != nil {
panic(err)
}
fmt.Printf("%+v", dbConfig)
// both Printf will produce: {Host:127.0.0.1 Port:3306 Auth:{Username:JohnDoe Password:verySecretPwd}}
}
Things that can be added to package, extended:
- Support more formats (like HCL)
- Add also a writer/persister functionality (currently you can only read configurations) to different sources and formats (JSONFileWriter/YAMLFileWriter/EtcdWriter/ConsulWriter/...) implementing a common contract like:
type ConfigWriter interface {
Write(configMap map[string]any) error
}
- Add a typed struct with methods like
GetString
,GetInt
...
- Feel free to use this pkg if you like it and fits your needs. Check also other packages like spf13/viper ...
- To run unit tests:
make test
/make cover
. - To run integration tests:
make test-integration
/make cover-integration
: will setup Consul and Etcd docker containers with some keys in them, run./scripts/teardown_dockers.sh
at the end to stop and remove containers). - To run benchmarks:
make bench
. - Project's class diagram can be found here.
This package is released under a MIT license. See LICENSE.
Other 3rd party packages directly used by this package are released under their own licenses.
- github.com/joho/godotenv - MIT License
- github.com/magiconair/properties - BSD (2 Clause) License
- gopkg.in/ini.v1 - Apache 2.0 License
- gopkg.in/yaml.v3 - MIT And Apache License
- github.com/pelletier/go-toml/v2 - MIT License
- go.etcd.io/etcd/client/v3 - Apache 2.0 License
- github.com/spf13/cast - MIT License
- github.com/actforgood/xerr - MIT License
- github.com/actforgood/xlog - MIT License